Technical Debt

Ward Cunningham's original metaphor was precise and is almost always misused. Debt, in his framing, is deliberately shipping a not-quite-right design to learn something sooner, with the intention of paying it back once you understand the domain better. The interest is the extra effort every subsequent change costs while the design remains wrong.

Most of what teams call technical debt isn't that. It's accumulated mess — code written under pressure, abandoned migrations, dead abstractions, and decisions that were reasonable in 2021 and haven't been revisited. That distinction matters because the two need different arguments. Deliberate debt was a trade the business made and can be asked to settle. Accumulated mess is a cost of doing business that has to be justified on its own terms.

Which brings us to the actual problem, which is rarely technical: engineers ask for "refactoring time" and get declined, repeatedly. The request is unpriced, open-ended, and framed as an engineering preference. The teams that successfully pay down debt do so by making its cost legible — this module causes 40% of our incidents, that coupling adds two weeks to every feature in the area — and by paying it down incrementally rather than asking for a rewrite.

TL;DR

Core Concepts

The four quadrants

Martin Fowler's classification, and it's genuinely useful for deciding how to talk about a given piece of debt:

Prudent deliberate debt is a real engineering tool — shipping a simpler design to learn faster, with an explicit intention to revisit. Prudent inadvertent debt is unavoidable: you understand the domain better after building it, and no amount of upfront design prevents that. The reckless quadrants are the ones that need process fixes rather than paydown plans.

Pricing it

The pattern is mapping debt onto something already being measured: incident rate, delivery metrics, onboarding time, compliance risk, or missed commitments. Nobody funds "cleaner code." Plenty of people fund "the thing that caused half our outages."

Where you can, attach a number even if it's rough. "Roughly two engineer-weeks per quarter of avoidable work" is a claim a finance-minded stakeholder can evaluate; "significant overhead" is not.

Measuring it

Signals that are cheap to collect and hard to argue with:

The hotspot analysis — files that change frequently and score badly on complexity — is the highest-value one. Complex code that nobody touches costs nothing. Complex code touched weekly costs continuously, and that intersection is where paydown pays back.

Which debt to leave alone

The last item in the "leave it" column deserves emphasis. Ugly, stable, well-tested code that hasn't changed in three years is not costing you anything. Refactoring it is pure risk with no return, and "it offends me" is not a business case. Engineering judgment here is largely about resisting the urge to clean things that don't need cleaning.

Paydown strategies

Opportunistic refactoring is the workhorse. It requires no separate approval, it concentrates effort where the code is actually being changed, and it means the cost is amortized into work that was already funded.

Why rewrites fail

The strangler fig pattern avoids this: put a facade in front of the old system, implement new functionality behind it in the new system, migrate existing functionality piece by piece, and delete the old code as it empties. Value ships continuously, the migration can be paused or abandoned at any point without losing everything, and you're never maintaining a fork.

Best Practices

Record deliberate debt when you take it on

An ADR or a ticket stating what shortcut you took, why, and what would trigger repaying it. Debt that's written down is debt that can be argued about later on its merits. Undocumented debt becomes indistinguishable from mess within a year.

Attach debt work to funded features

"To ship the new billing rules, we first need two weeks on the rate engine" is a scope conversation, which product managers have every day. "We need two weeks for refactoring" is a request for a gift, which they decline. Same work, entirely different reception.

Fix the process, not just the code

Recurring debt in the same shape means the process produces it — no design review, unrealistic deadlines, unclear ownership, a missing quality gate. Paying down the symptom without addressing that is a permanent tax. This is the highest-leverage move available and the one most often skipped.

Prioritize by change frequency, not by ugliness

The intersection of "changes often" and "hard to change" is where the interest is actually being paid. Sort your debt backlog by that, not by which code annoys the most engineers.

Make it visible

A dashboard showing incident attribution by service, cycle time by area, and build duration turns an argument into a chart. Leadership responds to trends far better than to assertions, and a rising line is a much easier ask than a description.

Get a standing allocation rather than asking each time

A negotiated 10–20% of capacity for maintenance and debt removes the recurring negotiation, which is where the energy gets lost. It needs explicit protection — the first bad quarter will absorb it unless someone defends it.

Set an end state for anything large

"Improve the billing service" runs forever. "The billing service uses the shared event bus and has no direct database access from other services, by Q4" is a project that can finish and can be judged. Open-ended cleanup loses funding.

Delete code aggressively

The cheapest debt reduction available. Unused features, dead code paths, abandoned experiments, and feature flags that have been at 100% for a year all cost comprehension and maintenance. Deleting them is fast, low-risk with good tests, and immediately reduces the surface area.

Common Mistakes

Asking for "refactoring time"

Proposing a rewrite

Fixing code nobody touches

Treating all debt identically

Not addressing the cause

Debt as a moral argument

FAQ

How much capacity should go to technical debt?

Commonly 10–20% as a standing allocation, higher temporarily when debt is actively blocking delivery and lower on a young codebase. What matters more than the exact figure is that it's predictable and protected — a nominal 20% that gets consumed by features every sprint is 0%. Some teams find it easier to defend as "maintenance and reliability" than as "technical debt."

How do I convince a product manager?

Translate into their terms and bring evidence. They care about predictable delivery, shipping dates, and customer impact — so show that a specific area's features consistently take two to three times longer, or that it produced most of last quarter's incidents. Then propose the smallest intervention that addresses it, attached to work they already want. Abstract arguments about code quality don't land; missed dates do.

Should we ever declare debt bankruptcy and rewrite?

Very rarely, and the honest cases are narrow: the technology is unsupported and unpatchable, the system can't meet a hard new requirement (a compliance regime, a scale change of an order of magnitude), or it's genuinely small enough to rewrite in weeks. Even then, prefer strangler fig so you're shipping value continuously and can stop without losing everything. The instinct that a rewrite will be faster is almost always wrong.

How do I stop debt accumulating in the first place?

Some accumulation is unavoidable — you learn things by building. What you can control: definition-of-done including tests and documentation, code review that catches structural problems rather than just style, a design review step for anything non-trivial, realistic deadlines, and clear ownership so nothing is nobody's problem. Recurring debt of the same kind is a process signal.

Is a legacy system with no tests all technical debt?

It's debt if it's costing you — if changes are risky, incidents are frequent, or delivery is slow because of it. A legacy system that runs reliably and rarely changes is just an old system. Where you do need to change it, add characterization tests around the behavior you're about to touch (pinning current behavior, not ideal behavior) and refactor incrementally from there. Michael Feathers' Working Effectively with Legacy Code is the reference.

Do AI coding tools make debt better or worse?

Both, and the balance depends on how they're used. They make refactoring, test generation, and comprehension of unfamiliar code substantially cheaper, which lowers the cost of paying debt down. They also make it very easy to generate large volumes of plausible code quickly, which can accumulate debt faster than any human team could — code nobody has fully read, with inconsistent patterns, and passing tests that were generated from the same misunderstanding. The mitigations are the ordinary ones: review, ownership, and a definition of done.

Related Topics

References