Technical Debt — What It Really Costs and When to Pay It Down
Technical debt isn't bad code, it's borrowed time. How to tell productive shortcuts from genuine liabilities, how to explain the cost to non-technical stakeholders, and how automation keeps debt from accumulating.
Technical debt is one of the few software metaphors that non-technical people immediately understand and developers routinely misuse. It doesn't mean "code I don't like". It means a deliberate or accidental shortcut that makes future change more expensive, and like financial debt, it isn't inherently bad. Sometimes borrowing is exactly the right call.
The problem is that most organisations never look at the balance, so they can't tell the difference between a sensible loan and a compounding liability.
Four kinds, and only two worth worrying about
Deliberate and prudent. "We know this won't scale past a thousand users, but we need to validate the idea and we'll rebuild it if it works." This is good engineering. Shipping an MVP is, by design, taking on debt in exchange for learning faster.
Deliberate and reckless. "We don't have time for tests." Said often enough, this is how teams end up unable to change anything safely.
Accidental and benign. You learned something about the domain that you couldn't have known at the start, and the old design no longer reflects reality. Unavoidable and normal.
Accidental and corrosive. Nobody understands why the code is like this, the person who wrote it left, and everyone works around it. This is the expensive kind.
The first and third categories are the cost of doing business. The second and fourth are what you need to find and address.
How debt actually shows up
Technical debt rarely presents as "the code is bad". It presents as business symptoms, which is why it so often goes unaddressed until it's severe:
- Estimates keep growing for superficially similar work. A change that would have taken two days last year takes a week now.
- Small changes cause unrelated breakages. A pricing tweak breaks the email templates. Nobody can explain why.
- Nobody will touch a particular area. There's a module the team routes around, and "we'd rather not change that" has become normal.
- Releases are frightening. Deployments happen at 10pm on Thursdays because everyone expects something to go wrong.
- Onboarding takes months. A competent new developer can't be productive because the system can only be understood by someone who lived through its history.
- The same bug keeps returning. Fixed three times, in three places, because the logic is duplicated.
- Manual steps have crept into the release. Someone runs a script, then updates a config by hand, then clears a cache.
If you're a business owner without a technical background, these are the signals to watch. You don't need to read the code — you need to notice that change is getting more expensive.
Making the cost visible
The reason technical debt goes unpaid is that its cost is diffuse while the cost of fixing it is concrete and immediate. Nobody wants to fund a sprint that produces no visible feature.
The way through is to quantify the interest payment. Ask your team, or your development partner:
- How much of each release goes to fixing things that broke elsewhere? If it's 30%, that's a third of your development budget servicing debt.
- What would this feature have cost two years ago? The difference is the interest.
- What are we not attempting because it's too risky? Often the largest cost, and the least visible — opportunity, not effort.
- How long from a developer finishing work to it being live? If it's weeks, that's process debt with a very direct commercial cost.
Framed that way, the conversation stops being "developers want to tidy up" and becomes "we are paying 30% of our development budget in interest, and here's how to reduce it".
Technical debt isn't a code quality problem. It's a rate-of-change problem, and rate of change is a business metric.
When to pay it down — and when not to
Pay it down when:
- You're about to build significantly on top of the affected area. Fix the foundation before you add floors.
- It's actively slowing every release, not just one corner of the system.
- It presents a security or compliance risk. This is not optional.
- Key dependencies are unsupported. Running an end-of-life framework or runtime is a deadline you don't control.
- The team can no longer deploy confidently.
Leave it alone when:
- The code is stable, rarely changed, and works. Ugly and untouched is cheap. Refactoring for aesthetics is a real waste.
- The feature may be retired. Don't renovate a room you're about to demolish.
- You're pre-product-market-fit and the debt isn't blocking you. Learning fast matters more right now.
- The only argument is that a newer framework exists. That's not a business case.
The honest test: is this debt making the work we want to do next more expensive? If not, it can wait.
How to actually do it
Wholesale rewrites are the most reliably disastrous approach in our industry. They take longer than promised, deliver no value until the end, and frequently reintroduce bugs that were fixed years ago in the system being replaced.
What works instead:
Pay as you go. Allocate a standing share of capacity — commonly 15–20% — to improvement work, and spend it in the areas you're already touching. Debt gets repaid where it's actually costing you.
Strangle, don't replace. Build the new implementation alongside the old, route a slice of traffic to it, expand gradually, then remove the old path. Every step is reversible and delivers value.
Fix the foundations before the feature. When a major piece of work is coming, budget the preparatory refactor as part of it rather than as a separate initiative nobody will fund.
Write the test first when you fix a bug. The cheapest moment to add coverage to a neglected area is when you're already in there.
Automation is the real prevention
Most debt accumulates because there's no cheap, continuous signal telling anyone it's happening. Manual review catches it occasionally; automation catches it every time.
The baseline worth having on any system you intend to keep:
- Automated tests running on every change, with the build failing when they don't pass. This is what makes change safe, and safety is what makes debt repayable.
- Automated dependency updates. Tools that open a pull request when a library releases a security patch turn an annual crisis into a weekly routine.
- Static analysis and linting in CI. Catches whole categories of problems before review.
- Automated deployment. If releasing is one button and takes minutes, you release often and in small pieces — which is itself the single best defence against debt.
- Error monitoring in production. Knowing what's actually failing, rather than waiting for a customer to report it.
Teams with this in place accumulate debt far more slowly, because the feedback is immediate and the cost of doing the right thing is low. Teams without it are relying on discipline, and discipline erodes under deadline pressure.
If you're inheriting a system
Taking over an existing codebase — from a previous agency, a departed developer, or an acquisition — deserves a deliberate assessment before you commit to a roadmap. Look for: whether it builds and deploys from a clean checkout, whether tests exist and pass, what dependencies are out of support, whether secrets are managed properly, whether there's any documentation, and how long a simple change takes end to end.
That assessment usually takes a week or two and is money extremely well spent. It's the difference between planning with information and planning with hope. Our guide to choosing a development partner covers what to ask when someone else has been maintaining your software.
Suspect your software is getting more expensive to change and want a straight answer? Get in touch — we do technical assessments of existing systems regularly, and we'll tell you plainly what's worth fixing, what's fine as it is, and what a rebuild would genuinely cost.