Skip to content
aipatterns.dev

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

CauseWhat it looks like in code
Deadline pressureCopy-pasted handlers, // TODO: clean up comments, flags that bypass validation
Changing requirementsA model built for one tenant now carries if (tenant === ...) branches everywhere. See Switch Statements
Missing testsNobody dares touch the module, so new code is bolted on beside it instead of into it
Missing shared understandingTwo teams build two Customer types with overlapping fields. See Alternative Classes with Different Interfaces
Delayed refactoringA function grows by ten lines per sprint until it is a Long Method
Long-lived branchesMerges that resolve conflicts by keeping both versions, creating Duplicate Code
SpeculationAbstractions for futures that never came. See Speculative Generality
Outdated dependenciesWorkarounds 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

  1. Pay interest where you work. The cheapest moment to fix structure is right before you change it. See When to refactor.
  2. 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.
  3. Add tests first. Untested debt cannot be repaid safely. Write characterization tests that pin current behavior before restructuring.
  4. Use small named steps. Each technique in Composing methods or Moving features between objects is a safe payment.
  5. Record what you defer. A tracked ticket or an ADR beats a TODO comment. 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

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.