Technical debt is the most abused metaphor in software, used to describe everything from a deliberate shortcut to code someone dislikes the style of. The original meaning is precise and worth recovering: you took a faster path knowing it was not the right one, and you now pay interest on that choice in the form of slower future changes.
Two things follow. Code that is ugly but never changes carries no interest and is not debt. And a shortcut taken deliberately, with a plan to repay, is a perfectly reasonable engineering decision — the problem is the shortcut taken accidentally and never named.
Measure the interest, not the principal
Nobody outside engineering will fund 'the code is bad'. They will fund 'changes to the billing module take three times longer than equivalent changes elsewhere, and four of the last six incidents originated there'. That is the same fact expressed as a cost, and it is measurable with data you already have.
# Hotspots: files that change constantly are where interest accrues.
# High churn plus high complexity is the list worth acting on.
git log --since='12 months ago' --name-only --pretty=format: \
| grep -v '^$' | sort | uniq -c | sort -rn | head -20
# Cross-reference with incidents and with review time. The module that
# appears in all three lists is the one to fund.
# - commits per file (above)
# - incidents tagged by area (your incident tracker)
# - median time from PR open to merge, by directory
# Coupling: files that always change together are an undeclared dependency.
git log --pretty=format:'%H' --name-only \
| python tools/co_change.py --min-support 12The co-change analysis is the one that surprises people. Two files that change together in forty commits are coupled regardless of what the architecture diagram says, and that coupling is usually the actual reason a change takes a week — not the code quality anyone was complaining about.
Pay it where you are already standing
Large dedicated refactoring projects have a poor record. They are difficult to justify, they compete with features and lose, and they carry real risk with no user-visible benefit. Incremental repayment — improving the area you are already modifying, as part of delivering something — is slower per unit but reliably actually happens.
- Repay in the hotspots only. Refactoring stable code is a cost with no return.
- Separate the refactor from the behaviour change into two commits or two pull requests, so review can verify each.
- Budget a standing percentage of capacity rather than negotiating each time. A negotiation happening every sprint will be lost most sprints.
- Write down deliberate shortcuts when you take them, with the condition that would trigger repayment. Undocumented debt becomes indistinguishable from design.
- Retire debt by deleting code wherever possible. The cheapest module to maintain is the one that no longer exists.
Nobody funds 'the code is bad'. Everybody funds 'this module has caused four incidents and doubles the cost of every change that touches it'.
The failure mode to avoid is the rewrite pitch. Proposing to rebuild a system is asking for a large budget, a long period with no new features, and a great deal of trust — and it competes with a strangler-style incremental plan that delivers continuously and can be stopped at any point. The incremental version is less satisfying to propose and considerably more likely to be approved, and finished.