Technical Debt: What It Is, How to Measure and Pay It Down
The bill for 'we'll fix it later' arrives six months on as lead times that have doubled and sprints consumed by unplanned work. What technical debt is and isn't, its types, how to measure it with signals instead of a single number (lead time, unplanned-work share, hotspot analysis, dependency age), the debt register, prioritizing by interest and principal, repayment strategies and how to explain it to management.
"Let's do it quickly now and fix it later." This sentence is spoken in every software project, and it is often the right decision: the deadline is real, the perfect design isn't. The problem is that "later" never comes and the decision accrues interest like a debt: six months on, a three-day change takes two weeks, half the sprint goes to unplanned work, and when the senior developer leaves nobody can touch that module. Management asks "why did we slow down"; the team says "technical debt"; nobody can give a number.
This article covers what technical debt is and isn't, its types, how to measure it with signals instead of a single number, the debt register and prioritization by interest and principal, repayment strategies, the guardrails that prevent new debt and how to explain debt to management. It's written for both team leads and the managers who approve the budget.
What technical debt is, and what it isn't
Ward Cunningham's metaphor: a design shortcut taken to ship faster today is a debt; until it's repaid, interest (extra effort) is paid on every change. The critical distinction: code that accrues no interest isn't debt. An ugly module that hasn't changed in ten years, works and is never touched is history, not debt. Debt is the shortcut in places that change often or produce incidents. Martin Fowler's quadrant separates the quality of the decision:
Deliberate | Inadvertent | |
|---|---|---|
Prudent | "We must ship now, we know the consequences, we recorded it" — healthy debt | "Now we know how we should have done it" — the natural debt of learning |
Reckless | "We don't have time for design" — debt taken knowingly and never recorded | "What's layering?" — debt from lack of knowledge |
A good team doesn't zero its debt; it keeps it prudent and recorded, and reduces the reckless kind.
Types
Type | Example | How the interest shows |
|---|---|---|
Code debt | Copy-pasted business rules, an 800-line service, magic numbers | One rule change means fixes in five places; regressions |
Architecture debt | No module boundaries, a shared database, circular dependencies | Small change, wide impact; teams wait for each other |
Test debt | No coverage, flaky/slow tests, manual testing | Fear on every release; long stabilization periods |
Dependency/infrastructure debt | Java 8, an unsupported framework, libraries never updated | Vulnerabilities, new libraries won't install, hiring gets harder |
Knowledge/documentation debt | A module only one person knows, no decision records | A standstill when the person leaves; the same debates repeated |
Security debt | Postponed patches, missing authorization checks | Audit findings, incident risk |
Data debt | Meaningless columns, inconsistent codings, unvalidated input | Reports don't reconcile; migration projects blow up |
How to measure it: signals, not a number
The "technical debt: 412 days" figure from static-analysis tools is attractive but misleading: it counts code smells, it doesn't measure interest. The real measure of debt is how much more expensive change and operations have become. A set of signals read together:
Signal | What it says | How it's collected |
|---|---|---|
Lead-time trend (idea → production) | The total interest of the debt | The work-tracking tool; monthly median and p85 |
Unplanned-work share | How much sprint capacity goes to firefighting | Work labels (bug, incident, urgent); target <20% |
Change failure rate | How many releases need a rollback/hotfix | Deployment records (DORA) |
Hotspot analysis (change frequency × complexity) | Where the interest is paid | Git history + complexity; the command below |
Dependency age and EOL count | The size of infrastructure debt and the security risk | Dependency scanning; the number of "end of life" items |
Test coverage on changed code, build time, flaky-test count | Test debt | CI reports; flaky-test rerun counts |
Incident tags | Which debt produces incidents | A root-cause tag on every incident (module, type) |
Developer friction survey | Interest that isn't measurable but is felt | Five questions quarterly: "What wastes the most time?" |
# Hotspots: the files changed most often in the last 12 months (change frequency); multiplied by complexity (lines, cyclomatic complexity)
git log --since="12 months ago" --format=format: --name-only -- src/main | grep . | sort | uniq -c | sort -rn | head -20
# Example output: 143 src/main/java/com/acme/order/OrderService.java ← changes often + 1,900 lines = priority one
# 97 src/main/java/com/acme/pricing/PriceCalculator.javaHotspot analysis is the most valuable measurement because it shows not all the debt but the part that accrues interest: 4% of the codebase typically carries 60% of the changes. That's where the repayment effort goes.
The debt register and prioritization
Debt is managed item by item, not as an anonymous "the architecture is bad" complaint. Every item: location (module/file), type, interest (hours lost per month or incident count — an estimate is enough), principal (repayment effort in days), risk (security, EOL date), owner, decision (repay / watch / accept). It lives in the work-tracking tool; architectural items link to an ADR (architecture decision record).
Item | Interest (month) | Principal (days) | Interest/principal | Decision |
|---|---|---|---|---|
OrderService (1,900 lines, hotspot #1) | ~40 hours | 15 | High | Repay: 2 sprints, alongside feature work |
Java 8 + Spring Boot 2.x | ~10 hours + security risk | 25 | Medium, high risk | Repay: this quarter (EOL) |
Reporting module (never changes, ugly) | ~0 | 20 | Zero | Accept; don't touch |
Manual regression testing (3 days/release) | ~24 hours | 30 | Medium | Repay: gradual automation |
The prioritization rule: high interest/principal ratio and in a hotspot first; EOL and security items by date; an item with zero interest, however ugly, sits at the bottom of the list.
How to pay it down
A continuous budget: 15–20% of capacity in every sprint for debt repayment; a continuous flow instead of waiting for a separate "debt sprint". Make the budget visible: on the dashboard and in the sprint goal.
Clean what you're about to change: Repayment focuses on hotspots and the code the next feature will touch; the "leave it a little better than you found it" rule is embedded in feature work. Rewriting code that doesn't change is waste.
Tests first, then refactor: Refactoring without tests isn't repaying debt, it's borrowing more; characterization tests are the first step.
Projects for big items: Framework migrations, module extraction, database splits are time-boxed, measured and done strangler-fig style; not big bang.
Dependency automation: Small, frequent updates with Renovate/Dependabot; major upgrades planned. The dependency-age metric drops, and the EOL crisis never happens again.
For knowledge debt: Pairing, module-ownership rotation, ADRs; the list of single-person modules is tracked as a management risk.
Guardrails that prevent new debt
// ArchUnit: architecture rules as tests — a violation breaks the PR, and the debate happens now, not later
@AnalyzeClasses(packages = "com.acme")
class ArchitectureRulesTest {
@ArchTest
static final ArchRule domain_must_not_depend_on_infrastructure =
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage("..adapter..", "org.springframework..", "jakarta.persistence..");
@ArchTest
static final ArchRule no_cycles_between_modules =
slices().matching("com.acme.(*)..").should().beFreeOfCycles();
@ArchTest
static final ArchRule controllers_must_not_call_repositories =
noClasses().that().resideInAPackage("..api..").should().dependOnClassesThat().resideInAPackage("..repository..");
}More guardrails: a CI quality gate on new and changed code only (the "no worse from today" principle instead of cleaning old code overnight); tests in the definition of done; deliberate debt decisions tagged in the PR with // DEBT: … (DEBT-142) and in the register; architectural decisions recorded as ADRs.
How to explain it to management
Management doesn't understand code smells; it understands three things: money, time, risk. Translate the interest into that language: "In a ten-person team, 25% of capacity goes to unplanned work and slowness; that's two and a half salaries" — then a measurable promise such as "if we repay these three items this quarter, unplanned work drops from 25% to 12%". Explain risk with dates: "The framework's security support ends in March; it becomes an audit finding." Pick three items per quarter, show the trend chart (lead time, unplanned-work share), report the difference after repayment. Trust comes after the first measurable win; a "let's rewrite everything" proposal ends it.
Common mistakes in the field
Anchoring on a single number: If the static-analysis "412 days" is falling while lead time is rising, the wrong thing is being measured.
The big cleanup sprint: A three-week "refactoring sprint" usually stops halfway and brings regressions; a continuous budget works better.
An ownerless register: The list grows, nobody repays.
Refactoring without tests: An incident instead of a repayment.
Beautifying code that never changes: Paying principal on a zero-interest item.
Postponing dependency debt until EOL day: A planned quarter of work becomes an unplanned crisis.
Making debt a moral issue: "What's the interest, what's the principal" instead of "who wrote this".
Frequently asked questions
When do you declare "bankruptcy" (a rewrite)?
When the interest is so high that paying the principal is impossible and the platform is truly dead; but that's rare, and a rewrite produces its own debt. In most cases the right answer is gradual repayment through strangler fig (see modernization).
Do AI coding assistants increase or reduce debt?
Both are possible: without rules, quickly generated and poorly understood code increases debt; used correctly for test writing, explanation and mechanical refactoring, they speed up repayment. The deciding factor is review discipline (see AI coding assistants).
How do you prevent debt in a new project?
You don't prevent it, you manage it: a quality gate, ArchUnit rules, ADRs and a debt tag from day one; every deliberate shortcut goes into the register. A zero-debt goal means never shipping.
Conclusion
Technical debt is inevitable and, managed well, a tool; unmeasured and ownerless, it's an interest burden that slowly brings the team to a halt. The right approach: find where interest is accruing with hotspots and delivery metrics, record it item by item, prioritize by interest and principal, repay with a continuous budget starting from the code you're about to change, limit new debt with guardrails and explain it to management in the language of money, time and risk.
To build your team's technical-debt register, run a hotspot analysis and set up a measurable repayment plan, see our software design and architecture and maintenance and support services.