Guide
Technical Debt
Technical debt is the extra cost every future change pays because of shortcuts and drift in today's code. Learn its causes and how to pay it down.
- Also known as
- Tech debt, Design debt
The metaphor
Ward Cunningham compared shipping imperfect code to taking a loan. You get something now (an earlier release) and you pay interest later: every change in that area takes longer than it would in clean code. The principal is the work needed to fix the structure. The interest is the extra time, bugs and risk paid on each change until you do.
Debt is a tool when you take it on purpose and track it; it becomes a crisis when nobody knows it exists.
Debt is not the same as bad code written by careless people. Most debt comes from code that was reasonable when written and then the world moved: requirements changed, the team learned more, a library evolved.
Where debt comes from
| Cause | What it looks like in code |
|---|---|
| Deadline pressure | Copy-pasted handlers, // TODO: clean up comments, flags that bypass validation |
| Changing requirements | A model built for one tenant now carries if (tenant === ...) branches everywhere. See Switch Statements |
| Missing tests | Nobody dares touch the module, so new code is bolted on beside it instead of into it |
| Missing shared understanding | Two teams build two Customer types with overlapping fields. See Alternative Classes with Different Interfaces |
| Delayed refactoring | A function grows by ten lines per sprint until it is a Long Method |
| Long-lived branches | Merges that resolve conflicts by keeping both versions, creating Duplicate Code |
| Speculation | Abstractions for futures that never came. See Speculative Generality |
| Outdated dependencies | Workarounds for bugs fixed upstream years ago |
How debt shows up
You rarely see debt directly. You see its interest:
- Estimates for small changes keep growing.
- A one-line fix needs edits in six files (Shotgun Surgery).
- One class changes in every pull request for unrelated reasons (Divergent Change).
- Bugs reappear after being fixed, because the logic exists in two places.
- New contributors, human or agent, make plausible changes that break hidden assumptions.
For an agent, debt has a specific cost: more context to load, more places to check, and more chances to produce a change that compiles, passes the visible tests and is still wrong.
Kinds of debt
- Deliberate and tracked. "We ship with a hard-coded currency list and replace it next sprint." Acceptable if the ticket exists.
- Deliberate and hidden. The same shortcut with no record. It becomes accidental debt for the next reader.
- Accidental. Nobody knew a better design at the time. You discover it later, usually when a change hurts.
- Bit rot. Many small, reasonable edits that together erode a once-clean design.
Paying it down
- Pay interest where you work. The cheapest moment to fix structure is right before you change it. See When to refactor.
- Prioritize by change frequency. Ugly code nobody touches costs almost nothing. Ugly code changed weekly is expensive. Version control history (
git log --format= --name-only | sort | uniq -c | sort -rn) shows the hot spots. - Add tests first. Untested debt cannot be repaid safely. Write characterization tests that pin current behavior before restructuring.
- Use small named steps. Each technique in Composing methods or Moving features between objects is a safe payment.
- Record what you defer. A tracked ticket or an ADR beats a
TODOcomment. Comments rot; see Comments.
Do not
- Do not schedule a "refactoring sprint" to fix everything. Big-bang cleanups stall feature work and rarely finish.
- Do not rewrite a working system to escape debt unless the platform itself is dead. A rewrite starts its own debt, with fewer tests.
- Do not refactor code you are not otherwise changing only because it looks bad. Spend effort where change happens.
- Do not mix paydown with feature work in one commit. Reviewers cannot tell which lines change behavior.
Agent checklist
- Before a feature change, check whether the target area shows smells that will make the change larger than it needs to be.
- Pay down only the debt in the path of the current task. Note the rest instead of fixing it.
- Confirm tests cover the code you restructure, or add characterization tests first.
- Keep paydown commits separate from behavior changes.
- When you take a shortcut, leave a tracked note with the limit and the trigger to revisit it.
See also
- What Is RefactoringRefactoring changes the internal structure of code in small, behavior-preserving steps so it becomes cheaper to read and change.
- When to RefactorRefactor right before you change code, while fixing a bug and during review. Skip it when the code is due for deletion or has no tests.
- How to RefactorRefactor in small named steps, run tests after each one, never mix in behavior changes, and stop when the code is clean enough for the task at hand.
- Shotgun SurgeryOne logical change forces small edits in many classes, so one missed edit ships an inconsistent system.
- Duplicate CodeThe same logic appears in two or more places, so every fix and change must be made several times and the copies drift apart.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.