Every small engineering team carries a mental list of things that need fixing. Not the exciting things. Not the features that show up in a demo. The quiet, structural problems that make every subsequent change slower and riskier than it should be.
The hard part is rarely identifying the debt. The hard part is justifying the time to pay it down when twelve other things could be built instead.
This post is about one refactoring decision we made, how we scoped it, and why it paid for itself faster than we expected.
We had a section of our codebase that handled tenant configuration. It worked. It had tests. But over eighteen months it had been extended by four different people, each solving the problem in front of them at the time. The result: a module with three overlapping patterns for achieving roughly the same outcome—deciding what a given tenant should see and how their data should flow.
New work in that area took longer than it should have. Every change required understanding all three patterns to know which one applied. Regression tickets showed up there more often than anywhere else. And when we onboarded a new engineer, this was the area that generated the most questions and the most confusion.
None of this was urgent. All of it was expensive.
Big companies write business cases. Small teams don't have that luxury, and they shouldn't need one. But "trust me, it's bad" is not a pitch. We found a lightweight way to make the case that took about thirty minutes of prep.
Step one: count the cost you already paid. We looked at the last two months of tickets. How many regression bugs traced back to this area? How many hours did those take to fix, test, and deploy? How many pull requests required more than one review round because the reviewer didn't understand the existing pattern? The numbers weren't catastrophic, but they were consistent. A slow bleed.
Step two: name the cost you're about to pay. We had two features on the roadmap that would require changes to the same area. Without a refactor, each would inherit the confusion. We estimated the delta: how much longer each feature would take if we built on top of the existing mess versus a cleaned-up version.
Step three: set a time box, not a scope document. We proposed one sprint. Not "until it's perfect." One sprint, with a clear definition of done: consolidate to a single pattern, update tests, leave documentation for the next person. Anything beyond that would wait.
The conversation took fifteen minutes. The answer was yes.
We deleted code. That was most of it.
Two of the three patterns were variations on the same idea, written at different times by different people who didn't realize the prior approach existed. We picked the one with the best test coverage, extended it to handle the cases the others covered, and removed the rest.
We wrote a short document—less than a page—explaining the surviving pattern: when to use it, how to extend it, and what constraints it assumed. Not architecture documentation for its own sake. A note for the next person, because there would always be a next person.
We updated the tests. Some were redundant. Some tested behavior that no longer existed. Some were missing. We ended with fewer tests that covered more actual behavior.
Two sprints later, we could measure the results without any special tooling.
Cycle time dropped. Pull requests touching that area went from an average of three review rounds to one. Engineers stopped asking in Slack which pattern to follow because there was only one.
Regression tickets stopped. Not reduced—stopped. Zero regression bugs in that area over the following six weeks. Before the refactor, we averaged two per sprint.
Onboarding got faster. We hired an engineer the month after the refactor. When they reached that part of the codebase, they read the document, looked at the tests, and started contributing. No folklore required.
You don't need a formal process. You need three things:
The refactor that pays for itself is not the grand rewrite. It's the focused cleanup that removes one source of confusion from every future change. The math is simple: save two hours per sprint on a team of four, and you've earned back a full sprint within two months.
Teams defer refactoring because it feels like a cost. But the status quo has a cost too—it just shows up spread across every ticket, every review, every onboarding conversation. The difference is that the status quo cost is invisible until you measure it.
Measure it. Make the case in thirty minutes. Set a time box. Do the work. Then move on and build the features you were going to build anyway—faster, with fewer surprises, and with a codebase your next hire can actually read.
Be the first to comment.
0 comments
Loading comments...