The "technical debt" metaphor is the most-borrowed concept in software engineering and one of the most misused. The original Cunningham definition was specific: shipping code that you know is not the long-term right design, deliberately, for short-term speed. The current usage has expanded to mean "code I don't like" or "code I would write differently now," which dilutes the framework and makes priorities harder.
This page is about distinguishing real debt from preference, the kinds that compound vs. the kinds that don't, and the practical work of managing it.
The useful definition: a deliberate or inherited choice that increases the cost of future change. Specifically:
Code can be old, ugly, or unfamiliar without being debt. Debt is what makes the next thing slow.
Different categories behave differently — they compound at different rates and respond to different repayment strategies.
The mechanism is suboptimal but works. A handwritten cache that should be Redis. A spreadsheet-driven workflow that should be automated. The functionality is correct; the implementation is brittle or inefficient.
Compounding rate: low. Implementation debt that works tends to keep working.
When to repay: when the implementation actively hurts (downtime, cost) or when a related change makes the work cheap.
The structure is wrong for the current requirements. A monolith that should be a few services, or vice versa. Coupling between modules that should be independent. Boundaries that no longer match the problem.
Compounding rate: high. Each new feature has to navigate the wrong structure; the cost of new work grows over time.
When to repay: when feature velocity has noticeably slowed, when the structure is preventing changes that have business value.
The team that knows the system has shrunk. Documentation is sparse. The original engineers have left. Specific decisions cannot be explained.
Compounding rate: high. Each successive engineer has less context; mistakes happen because the constraints are forgotten.
When to repay: continuously. Document decisions as they happen. Train new people on existing systems before the original team is gone.
Test coverage is sparse or unreliable. CI catches some bugs but misses many. Refactoring is risky because behavior is not pinned down.
Compounding rate: medium-high. Each ungated change has the potential to introduce regressions; the codebase becomes harder to evolve.
When to repay: when bug rate or regression rate is unacceptable, or before any major refactoring.
Libraries, frameworks, and runtime versions are outdated. Security patches require upgrading; breaking changes have accumulated.
Compounding rate: low until it becomes catastrophic. The longer dependencies sit, the harder the upgrade.
When to repay: continuously, in small steps. Big-bang version upgrades are notoriously painful; small steady updates are tractable.
A common misuse: calling something debt because it does not match current preference.
Real debt produces measurable friction. If there is no friction, there is no debt.
Different debts have different ongoing costs:
| Debt type | Direct cost | Indirect cost |
|---|---|---|
| Implementation | Operational (perf, cost) | Lower |
| Design | Slower feature delivery | Compounds |
| Knowledge | Mistakes in future changes | Compounds |
| Test | Regressions | Compounds |
| Dependency | Security risk | Bumps to catastrophic |
The compounding categories are where most of the real cost lives. A team carrying significant design or knowledge debt sees its delivery rate drop over time without any single visible cause.
The honest framework:
Most teams repay too much of category 5 (cosmetic) and not enough of category 1–3 (urgent and high-value).
Pay the small debt as you make changes nearby. The "boy scout rule" — leave the campsite cleaner than you found it. Works for small fixes; doesn't scale to design debt.
A planned project to repay specific debt — refactor a module, upgrade a framework, document a system. Works for medium-scale debt with a clear endpoint.
Major design changes that take significant time. Use the strangler pattern and expand-and-contract. Plan for months; check in regularly.
Sometimes the right answer is to remove the debt entirely — retire the system, delete the unused code path, kill the feature. The "we'll fix it eventually" promise often lives longer than the system itself.
Engineers are often bad at explaining debt to non-engineers. The translations that work:
Generic "we have a lot of technical debt" is rarely persuasive without specifics.