Skip to content
ApexSutra

Engineering

The real cost of technical debt

Technical debt is not messy code. It is the gap between what your system assumes and what your business now needs — and it is priced in decision latency.

ApexSutra EngineeringUpdated 8 min read

"Technical debt" has become a synonym for code somebody dislikes. That reading makes it impossible to prioritise, because taste is not a business case.

A more useful definition: technical debt is the distance between the assumptions your system was built on and the reality it now operates in. Code can be ugly and carry no debt — if nothing about its context has changed, it is simply working. Code can be pristine and carry enormous debt, if it was designed around a single-tenant model and you now sell to enterprises who require isolation.

This is closer to the original metaphor than the way it is normally used. Ward Cunningham, who coined the term in 1992, spent years correcting the drift, and Martin Fowler's technical debt quadrant (October 2009) is blunter still: "a mess is not a debt... messy code, produced by people who are ignorant of good design practices, shouldn't be a debt." Debt, in the original sense, is what you take on knowingly to ship sooner — or what accrues as your understanding of the domain improves and the code does not.

The distinction matters because it tells you which debt to pay down: the debt sitting between you and something you actually need to do.

What it actually costs

The cost is not "slower development" in the abstract. It shows up in specific, measurable places.

Decision latency. The clearest symptom of expensive debt is how long it takes to answer "can we do X?" In a healthy system that is a short conversation. In an indebted one it becomes an investigation, because nobody is confident what a change touches. That delay compounds across every roadmap discussion.

Estimate variance, not estimate size. Debt does not make estimates bigger so much as it makes them unreliable. A team that says "two weeks" and lands within a few days is workable. A team that says two weeks and takes six, unpredictably, cannot be planned around — and that unpredictability is what erodes trust between engineering and everyone else.

Onboarding time. How long before a new engineer ships to production unsupervised? When that stretches past a month, essential knowledge usually lives in people rather than in code. That is debt with a retention risk attached.

Change amplification. One logical change requiring edits in six unrelated places is the most honest signal of all. It means the structure does not match the domain, and it predicts where the next bug will be. It is also the signal most often misread as an argument for splitting the system into services — which, as we have argued about microservices, usually moves the coupling onto a network rather than removing it.

The order of magnitude

The most-cited attempt to size this is Stripe's Developer Coefficient, a September 2018 survey of more than 1,000 developers and 1,000 C-level executives. Respondents reported spending 13.5 hours of an average 41.1-hour week on technical debt, plus a further 3.8 hours on bad code — roughly 42% of the week going to maintenance rather than new work.

Where an average developer week goes

A 41.1-hour working week divided into 13.5 hours on technical debt, 3.8 hours on bad code, and 23.8 hours of other work, as self-reported in Stripe's 2018 Developer Coefficient survey.

41.1-HOUR AVERAGE WEEK

13.5h23.8h

Technical debt (33%)

Bad code, 3.8h (9%)

Other work (58%)

Source: Stripe, The Developer Coefficient (September 2018), n > 1,000 developers across five countries. Self-reported; see the caveat below.

Treat the precision carefully. Those numbers are self-reported, they are now several years old, and "technical debt" meant whatever each respondent took it to mean — which, given the definitional problem this post opens with, is a real weakness in the data. The useful part is not 13.5 hours; it is the order of magnitude. Roughly a third of engineering capacity is the figure worth arguing about, and no team we have worked with has been surprised by it.

The debts worth paying

Not all of it should be repaid. Debt in code that never changes costs you nothing — refactoring a stable module is an expense with no return.

Prioritise where these overlap:

  1. The change is slow — measured, not felt.
  2. The area changes often — check the commit history rather than trusting impressions.
  3. Something on the roadmap requires it — there is a real thing this unblocks.

Where all three are true, the work has a business case that does not require anyone to care about elegance.

The second criterion is the one teams skip, and it is the cheapest to check. git log over the last year, grouped by directory, will usually disagree with the team's intuition about where the churn is. Debt in a file nobody has touched since 2023 is not on your critical path, however much it offends.

How to fund it

The two common approaches both tend to fail.

A dedicated refactoring sprint gets cut the moment a deadline appears, because it is the only item with no customer-visible outcome. A fixed percentage of every sprint sounds disciplined but decays into a slot people fill with whatever is bothering them that week — which is taste-driven again.

What works better is attaching it to delivery. When a feature requires touching an indebted area, the cleanup goes into that feature's scope and estimate. It becomes part of doing the work properly rather than a competing priority, and the debt paid is by definition debt that was in the way. This is why our delivery process prices remediation into feature estimates rather than tracking it as a parallel backlog — a separate backlog is a queue that only ever grows.

Where it comes from

Some debt is a deliberate trade — ship now, generalise later — and that is legitimate engineering, provided it is recorded. Fowler's quadrant calls this prudent and deliberate, and it is the only quadrant that is genuinely a debt in Cunningham's sense. Most debt is not a decision at all. It accumulates because a choice was made for a context that quietly changed, and nobody wrote down what the choice assumed.

Which is the argument for architecture decision records, and the reason we insist on them: not documentation for its own sake, but so that when the context shifts, the affected decisions can be found. Debt you can locate is debt you can price. Debt nobody can explain is the expensive kind.

Questions we get asked

Is technical debt ever worth taking on deliberately? Yes, and the original metaphor assumes you will. Shipping a narrower design to learn something from real usage is prudent debt. The condition is that you record what you assumed and what would make you revisit it — otherwise it stops being a trade and becomes an accident waiting to be discovered.

How do I convince finance or a board to fund this? Stop describing it as debt. Describe the roadmap item it blocks and the decision latency it causes. "We cannot offer per-tenant data isolation, which we lose enterprise deals over, and the reason is a schema assumption from 2022" is a business case. "The code is messy" is not.

Should we rewrite instead? Almost never as a first answer. A rewrite replaces a system whose failure modes you know with one whose failure modes you do not, and it pays nothing back until it ships. Extract and replace the specific area that meets all three criteria above, and keep shipping while you do it.

What if the debt is in something we plan to retire? Then it is not debt worth paying. Retirement is the cheapest possible repayment, and it is routinely overlooked — the same logic applies to what you migrate to the cloud, where the cheapest workload to move is the one you switch off instead.

Keep reading

Facing one of these decisions right now?

A first conversation is free and useful even if you go elsewhere. You'll leave with an opinion you can act on.

Book a consultation