Balancing Speed and Stability: When to Refactor and When to Ship
There is a characteristic in the heart of a critical software application that any developer would recognise as a mess. That characteristic, or clue - is that the application has been “temporary” for three years, and while it works; no one has much affection for it. The options are plain: put some order into it or let it be.
Then comes the question from the business side that developers tend to avoid: “Can we just ship it?”
On occasion, that answer could be “Yes”, but most often it should be “No”.
Technical debt is not unimportant, but software development is as much a business concern as a technical one. There are deadlines to keep and customers with expectations; competitors do not wait and budgets have their limits.
So the issue is not whether technical debt is inherently good or bad. It is a matter of knowing when refactoring is worth the time and when the wiser course is to ship, monitor and return to it down the line. In striking that balance you will find what sets a capable engineering team apart from those who simply go fast until they break.
One of the keys to managing Technical Debt is being able to measure it, and you can find more on that topic in our article: How To Measure Technical Debt
Technical Debt Is a Business Decision
Once you understand what Technical Debt is, you will see that it is often treated as a problem for the developers to sort out, but it is often more than that. There is a cost to every shortcut and a cost to every bit of tidying up and that also makes it a business decision as well. A team may forgo refactoring and put a feature in front of a customer two weeks ahead of schedule, which is no small thing if it means meeting a market deadline. But should that expediency make all future work more arduous, the organisation could end up expending far more on maintenance over the course of the year.
In that sense technical debt is like financial debt. Borrowing can get things done sooner and is not in itself a disaster; trouble only arises when the repayments become too onerous.
To put a refactoring decision in perspective, a team might ask:
- Is the code responsible for outages or security issues?
- Will it impede what we need to do next?
- How often are we having to put our hands on it?
- What opportunity are we putting off by refactoring now?
- And does the cleanup offer a tangible reduction in operational cost?
This puts the discussion on a more productive footing. Rather than “The developers want it cleaner,” - the topic becomes “Which path yields the better result for the business.”
When Is the Investment Justified?
Not all unsightly code needs to be made over. Some of it is harmless enough, quietly doing its job in a corner of the app for years. While not pretty or clean, it works well even when stressed. A refactoring effort there might be gratifying for the developers but adds little value to the bottom line.
Other components of an application might be another matter entirely. If a piece of code is in the way of new development or is a source of defects, errors or poor performance, then a code cleanup will show a return.
When considering the cost of a code change, a developer's productivity is a good indicator of whether it will be a good investment. Whereas a team would ordinarily require two days to alter a feature, convoluted underlying code can stretch that to five. Do that repeatedly and the toll is considerable. Consider the case of a five-day task that can be whittled down to two with some refactoring; such an undertaking can recoup its costs in short order. The math is straightforward:
Refactoring ROI = Expected future savings minus Refactoring cost
Take an 80-hour developer effort for instance. If it is going to put 20 hours back in the monthly budget for the year ahead, the numbers justify the investment. One has to make certain assumptions, and that software estimates are no more reliable than fortune cookies; but putting a figure on a refactoring job is preferable to having an emotional debate over technical debt.
Refactor Before the Pain Gets Expensive
There is value in timing. A team would be unwise to let technical debt run its course until it brings down production. By then you are looking at emergency development, unhappy customers and damage to your reputation, not to mention lost revenue. Some warning signs should be heeded right away:
- A minor change that keeps breaking things it shouldn’t
- Test failures that are hard to pin down
- A component that developers will not touch for fear of the risk
When performance issues start to show for the customer or compliance needs lay bare a design flaw, refactoring ceases to be mere housekeeping and becomes a form of risk reduction. That has business value.
When Shipping Is the Better Decision
Then there are occasions to hold off on refactoring. In the run up to a critical launch a team might find code that is less than elegant. Provided it is not an operational hazard and it passes its tests, devoting a month to a redesign is poor resource management.
Good enough is good enough, particularly if the code has a limited shelf life. No one spends hours fitting out a temporary office with top tier cabinetry, and the same holds for software.
If a code section is to be superseded in three months, any architectural work done now is wasted. The key is to tell the difference between a temporary imperfection and dangerous debt. The latter has a way of growing teeth.
Defining “Good Enough” Code Quality
The term “Good Enough” cannot simply mean “nobody has made an issue of it yet.” Objective criteria are called for. Reliability, security, test coverage and maintainability are all part of a practical definition of quality - though the bar will be set higher for a payment system than for an internal prototype with six users. Business risk should dictate the standard.
A Simple Quality Framework
Prior to shipping, four questions should be asked of the code:
Does it work? - It should do what it is meant to and stand up to testing.
Is it safe? Any risks around privacy, compliance or reliability must be in order.
Can it be supported? - When something goes awry, operations and developers need to be able to make sense of it.
Can it be changed? - The code should not be so brittle that routine work down the line comes at a premium.
If those are answered in the affirmative, the code is acceptable even if it lacks beauty. The aim is not perfection but useful, reliable software.
The Hidden Cost of Refactoring
There is an opportunity cost to refactoring, no matter the benefits. An hour devoted to reorganizing old code is an hour not put toward something new, a fact that can be easy to miss when a developer is faced with a particularly unappealing codebase.
Consider a six-week refactoring effort: it may do wonders for maintainability. Yet if the team in question could have put a revenue-generating feature in front of customers in that time, the organization has to weigh that tradeoff. This is not to say new features must always come first, but engineering work has to be judged on its impact.
A mature team will sort technical debt into clear categories to make prioritization straightforward and to stave off the tendency for developers to chase their own pet refactoring projects. The list might go like this:
- Critical: A risk to production, security or compliance.
- High: Recurring defects or a drag on development.
- Moderate: Harder to maintain but not an operational threat.
- Low: Readability or architecture.
Making Technical Debt Visible
Feature requests are straightforward; a product manager can show you the new capability and what the customer gets. Technical debt is more intangible and all too often ignored for that reason.
Teams should track metrics to put a number on it: change lead time, failed deployments, incident frequency, or how much time is lost working around known limitations. If one service is prone to deployment failures, there is a measurable cost to be had. That kind of evidence makes a case for refactoring.
Refactoring as Part of the Process
It doesn’t have to be a massive undertaking. While building a feature, a developer can improve a poorly structured module rather than leave it be.
Call it the “boy scout rule” – leave the code in better shape than you found it. It requires discipline, not a six-month rewrite. One person will tidy up a function, another a test, and over time these small acts keep technical debt from becoming unmanageable.
Refactoring Versus Rewriting
One must distinguish between the two. Refactoring alters the internal structure while leaving external behavior intact. Rewriting is a replacement.
When a codebase is maddening, a rewrite is tempting. Developers picture a fresh start and better architecture. But then reality sets in. You run into years of undocumented business rules and odd behaviors nobody recalls, and the project drags on.
A refactor of the code is the safer bet for a system that works but is hard to live with. A rewrite is only warranted if the architecture is blocking the business entirely, and even then the decision should rest on hard data, not the observation that “everyone hates this code.”
Using a Decision Matrix
If the path forward isn’t clear, teams can score it based on business impact, technical risk, how often the code changes and the effort required. Code supporting a revenue product that is altered weekly and breaks regularly is a higher priority than something untouched for two years.
A matrix will show the difference:
Factor
Low Concern
High Concern
Business impact
Internal tool
Revenue-critical feature
Change frequency
Rarely touched
Changed constantly
Technical risk
Stable
Frequent failures
Refactoring effort
Few hours
Several months
Don’t Confuse Velocity with Speed
There is pressure to increase velocity – more features, shorter cycles. But raw speed can be deceptive. A team that puts out ten features only to spend the next quarter on production issues has not necessarily done better than one that delivered seven stable ones.
Stability is part of the equation. If poor observability means a bug takes three days to pin down, that is a real cost. Engineering leaders would do well to look at deployment frequency, mean time to recovery and defect escape rates to see if a team is truly efficient or just piling up trouble for later.
Guardrails for “Ship It”
“Ship it” is a fine attitude until it becomes hazardous. Automated testing, feature flags and rollback procedures are guardrails that make fast delivery less risky. With the ability to roll back quickly, an organization can tolerate a degree of technical uncertainty it couldn’t with a high-risk system pushed to everyone at once.
Good engineering is about managing risk, not doing away with it.
Put a Budget on Technical Debt
Some organizations set aside capacity for it - maintenance, refactoring, architectural work. The principle is what counts. Left to its own devices, technical debt will lose every scheduling argument to the new feature with a visible deadline and stakeholder.
A budget for technical debt is a healthier way of running things. It treats maintenance as standard engineering work instead of an optional extra.
The Whole Team Should Make the Call
When it comes to refactoring, the decision is not one for developers or executives to make in isolation. The ideal outcome is a synthesis of all the different vantage points in the room: the technical implications known to the developers, the customer focus of product management, the revenue and risk considerations of business leadership and the production realities of operations.
Take a developer who puts forward the case that “This module is getting hard to maintain.” A product manager can add weight to that by pointing out three major features in the works that will be built on top of it. Suddenly, there is evidence rather than just opinion, this is a real value of the team being involved in the decision.
Timing Is Everything
There is no one size fits all when determining the right moment to refactor, but a good rule of thumb is to do it when the cost of inaction down the line will outweigh the effort of making an improvement today. It is easy to say, though teams have trouble putting it into practice since a present deadline is far more visible than some future expense.
One has to look for patterns. When you see development decelerating, workarounds being cobbled together and defects turning up, the debt is coming due and someone ought to settle the bill. On the other hand, if the code is stable, well tested and set to be replaced, it is perfectly rational to leave it be. Not every bit of mess calls for a fix.
Conclusion
A strong software team does not view speed and stability as adversaries. Shipping fast is what creates value; refactoring is what ensures you can keep doing so. The trick is in the prioritisation.
Evaluating technical debt on its own merits is not enough. Look at the business impact, the risk, the return and how often things are changing. Put in place quality guardrails and some capacity for maintenance. Define what “good enough” is for the system at hand.
Above all, refactoring needs to be done for a reason. Not because the code lacks aesthetic appeal, but because it serves to lower costs, reduce risk or advance a business goal. And shipping is not an excuse to put your head in the sand about problems. There are times when the sensible thing is to ship and put in place a plan to address it tomorrow. That is not cutting corners, it is engineering with context.
For more information about managing Technical Debt, or having us work on your technical debt, please feel free to contact ScreaminBox.
Check out our Podcast on Scaling SaaS Product Development and Engineering - for a deeper look at development engineering and technical debt.