Technical debt means the extra effort a change costs because the code isn’t in the shape it should be. A function you can’t follow, logic copied into more than one place and a type check switched off all make the next change slower. Call that slowdown the interest. Reworking the code into a better shape repays the principal.

Ward Cunningham coined the debt metaphor in 1992, for code that reflected what his team understood at the time and needed reworking as they learned more. Some debt of that kind can’t be avoided. The code-level kinds, such as duplication and tangled functions, are ones a linter can see, so you can stop adding them by making the build fail on them. A linter is a tool that checks code against rules without running it.

Technical debt definition

Martin Fowler’s Technical Debt (2019) uses the word cruft for flaws in a system’s internal quality that make it harder to change than it needs to be. In his terms, interest means the extra effort each new feature takes because the cruft is there. His worked example puts numbers on it:

  • A feature takes four days in a clear module structure and six in a confusing one. The two extra days are the interest.
  • Cleaning up the structure takes five days, which is the principal.
  • For one feature, cleaning up first means nine days instead of six. With two more similar features to come, it’s quicker overall.

A 2016 seminar at Schloss Dagstuhl produced the research definition. Paris Avgeriou, Philippe Kruchten, Ipek Ozkaya and Carolyn Seaman wrote it up in Managing Technical Debt in Software Engineering (2016). It describes technical debt as shortcuts in design or code that help in the short term but make later changes more costly, or impossible. Its effect is limited to the system’s internal qualities, mainly how easy it is to maintain and evolve. Users never see the debt itself, only changes arriving more slowly.

Tech debt is short for technical debt. Code debt means debt in the source itself, such as unreadable or duplicated code. It’s one of the types described below.

Terese Besker, Antonio Martini and Jan Bosch surveyed 43 developers and interviewed 16 for Technical Debt Cripples Software Developer Productivity (2018). The developers lost 23% of their development time to technical debt on average, and existing debt often forced them to add more.

Where the term comes from

Ward Cunningham introduced the metaphor in The WyCash Portfolio Management System, an experience report for OOPSLA, the object-oriented programming conference, dated 26 March 1992. WyCash managed portfolios of short-term securities and was written in Smalltalk. His team grew it from a working prototype and rewrote its mature parts many times as their understanding improved.

His report names the more serious danger as failing to consolidate that understanding back into the code. “Shipping first time code is like going into debt,” it says. Taking on a little speeds things up, as long as the team pays it back soon by rewriting the code. Time spent on code that’s not quite right counts as interest, and enough unpaid debt can stop a whole engineering organisation. The phrase “technical debt” never appears, and Cunningham calls the idea a debt analogy.

In a five-minute video recorded in 2009, transcribed on his wiki, he says he used the metaphor to explain his team’s refactoring to his boss. Refactoring means changing the structure of code without changing what it does. This was financial software, so he chose a financial analogy. When the program stopped matching what the team had learned about the financial objects, they kept stumbling over the mismatch. That slowed them down, like interest on a loan.

I’m never in favor of writing code poorly

— Ward Cunningham, Debt Metaphor, 2009

He says in the same video that many bloggers confused the metaphor with writing code badly and planning to fix it later. What he favoured was code that reflects your current understanding of a problem, even a partial one, kept clean enough to refactor as that understanding grows.

Types of technical debt

Fowler’s Technical Debt Quadrant (2009) sorts debt by two questions. Was it taken on deliberately or inadvertently, and was that prudent or reckless? He wrote it in reply to Robert C. Martin, who had argued that a mess made by people who don’t know good design shouldn’t count as debt. Fowler counts it as reckless debt, with crippling interest.

Reckless Prudent Deliberate Corners cut The team knows the better way and skips it to save time. A planned loan A shortcut taken knowingly, with a plan to pay it back. Inadvertent Didn’t know The team doesn’t know the better practice exists. Learned later The better design shows up only once the code exists. Cunningham’s kind
After Martin Fowler's Technical Debt Quadrant (2009). Cunningham's kind of debt sits in the prudent, inadvertent cell.

Fowler argues that the prudent, inadvertent cell is inevitable for good teams, who learn what the design should have been by building the system, sometimes a year in. He ties it to the debt Cunningham describes in his video. A tool won’t flag it, because the code may be perfectly clean. The team pays it down by refactoring as it learns.

You can also sort debt by where it lives. Marion Wiese’s A Practical Guide to Establishing Technical Debt Management (2026) lists code, architecture, documentation, test, infrastructure, requirements, build and security debt among its types. It notes that researchers are still finding more. Her list builds on Zengyang Li, Paris Avgeriou and Peng Liang’s mapping study of the research (2015). That study found code debt gets the most attention.

Technical debt examples a linter can catch

Tools can find code debt. The Prickles packs fail the build on the kinds in the table, each with a tenet that argues the fix. Cyclomatic complexity, in the second row, counts the paths through a function. It starts at one and adds one for each branch or condition.

Debt What it looks like What catches it Tenet
Duplication Logic repeated in two places, so a fix lands in one and not the other jscpd, or PMD’s CPD for Java and PHP, in CI, on copies of 50 tokens or more TA2 Duplication Detection
Complexity Conditions nested inside conditions A cyclomatic complexity limit of 3 in ESLint, Ruff, PMD and PHP_CodeSniffer S2 Cyclomatic Caps
Type escape hatches any, unchecked casts and @ts-ignore stop the compiler checking no-explicit-any and ban-ts-comment in TypeScript, Ruff’s ANN rules in Python T2 No Escape Hatches
Comments doing the code’s job A comment explaining what unclear code does, or a TODO admitting a shortcut A comment rule in every pack, which fails any comment but a tool directive, an SPDX line or a generated-file banner F5 Self-Documenting Code
Magic numbers A bare 5000 with no name to say what it means A magic-number rule in every pack S5 Inline-First

Here’s what those rules make of a delivery-charge function written in a hurry. It accepts an order of any shape, nests one condition inside another and writes its prices as bare numbers in pence. It explains its £100 rule in a comment on the same line as the code. Its first line is a TODO. ESLint 10.12.0, run with release 3.0.0’s TypeScript pack on 13 October 2026, fails it with 15 errors:

src/charge.ts
   1:1   error  Remove the comment                                                       prickles/no-comments
   1:1   error  Unexpected 'todo' comment: 'TODO: tidy this up after the sale'           no-warning-comments
   1:4   error  Complete the task associated to this "TODO" comment                      sonarjs/todo-tag
   2:8   error  Missing return type on function                                          @typescript-eslint/explicit-function-return-type
   2:8   error  Function 'charge' has a complexity of 4. Maximum allowed is 3            complexity
   2:27  error  Unexpected any. Specify a different type                                 @typescript-eslint/no-explicit-any
   4:23  error  No magic number: 10000                                                   @typescript-eslint/no-magic-numbers
   5:17  error  Remove the comment                                                       prickles/no-comments
   5:17  error  Unexpected comment inline with code                                      no-inline-comments
   5:17  error  Expected comment to be above code                                        line-comment-position
   7:5   error  Return a ternary rather than a guard that only the final return follows  no-restricted-syntax
   7:12  error  No magic number: 999                                                     @typescript-eslint/no-magic-numbers
   9:21  error  No magic number: 5000                                                    @typescript-eslint/no-magic-numbers
  12:3   error  Return a ternary rather than a guard that only the final return follows  no-restricted-syntax
  12:10  error  No magic number: 499                                                     @typescript-eslint/no-magic-numbers

✖ 15 problems (15 errors, 0 warnings)

Each error names a piece of debt and the rule that found it. sonarjs/todo-tag comes from SonarJS’s recommended rules. The pack switches those on too. prickles/no-comments, the pack’s own rule, fails every comment. The pack’s no-restricted-syntax rule asks for a ternary where a guard is followed only by the final return.

Paying the debt off puts the prices in one table with names and gives the order a type. After that, the comment has nothing left to explain. The function’s complexity falls from 4 to 2 and the same rules report nothing:

Viewing: TypeScript.
type Speed = "standard" | "express";

type Rate = { readonly chargePence: number; readonly freeFromPence: number };

const RATES: Readonly<Record<Speed, Rate>> = {
  standard: { chargePence: 499, freeFromPence: 5000 },
  express: { chargePence: 999, freeFromPence: 10_000 },
};

export type Order = { readonly subtotalPence: number; readonly speed: Speed };

export const deliveryChargePence = (order: Order): number => {
  const rate = RATES[order.speed];
  return order.subtotalPence >= rate.freeFromPence ? 0 : rate.chargePence;
};

Every version charges 499 pence for a standard order of £49.99 and nothing from £50. An express order costs 999 pence below £100 and nothing from £100.

How to stop adding technical debt

  1. Write each limit into the linter as an error. A warning lets the build pass, so nothing makes anyone fix it.
  2. Run the linter, type checker and duplication check in CI, the checks that run on every push. A change that adds debt then can’t merge.
  3. Run the same checks before each commit, while the change is still in front of you.
  4. On an existing codebase, record today’s errors as a baseline and fail the build only on new ones.
  5. When a rule is wrong for your code, change it in the config and write down why. Don’t switch it off with a comment on the line.

These steps come from TA1 Linter as Law. Its rule for agents opens: “tooling enforces what prose suggests. If the rule matters, encode it.”

Step 4 is how an older codebase gets started without fixing everything first. ESLint has had bulk suppressions for it since version 9.24. The first command records every current error. The second removes entries for errors you’ve since fixed:

npx eslint --suppress-all .
npx eslint --prune-suppressions .

ESLint keeps the record in eslint-suppressions.json, counted per file and per rule, and stops reporting those errors on later runs. In a test with ESLint 9.39.5 on 12 October 2026, one new error in a file was enough to fail the run. ESLint then reported every error of that rule in that file. Fixing an error failed the run too, with a message to prune the suppressions. So the recorded count can only go down.

How to reduce technical debt you already have

Fowler’s advice in Technical Debt is to pay the principal off gradually, a little with each feature. Cleaning up where you’re already making changes puts the work into the code that changes most. Interest runs highest there. Messy code that nobody touches costs nothing until someone has to change it, so it can wait.

P6 Leave it Better Than You Found it turns that into a habit. Its rule for agents reads “tidy what you touched, no further. Behaviour changes and tidies travel in separate commits.” Tidy the file you’ve opened for the change, in a commit of its own, so a reviewer can read the clean-up apart from the change in behaviour.

To choose where to start, compare two lists. The suppressions file shows the recorded errors for each file. Your version history shows which files change most often. Files near the top of both are likely to be where the team pays the most interest. This lists the files changed most often:

git log --name-only --format="" | sort | uniq -c | sort -rn | head

Managing technical debt in agile teams

Cunningham’s 1992 report was itself an argument for building in small increments. He set his team’s incremental growth against the waterfall habit of working a program out in detail before coding. In his debt terms, that meant paying up front and in full. Incremental growth works only if the team keeps repaying as it learns, and in the 2009 video he calls that repayment refactoring.

Scrum handles this through the Definition of Done. Ken Schwaber and Jeff Sutherland’s Scrum Guide (2020) describes it as a formal description of the quality an increment must meet. An item that doesn’t meet it can’t be released. With the linter, type checker and duplication check in your Definition of Done, work that adds code debt can’t count as done. P3 Definition of Done turns it into a single command. Done means the checklist passed.

AI technical debt: how coding agents add it

Coding agents let a team write more code, at least at first. The studies so far find more debt with it:

  • Hao He, Courtney Miller, Shyam Agarwal, Christian Kästner and Bogdan Vasilescu wrote Speed at the Cost of Quality (MSR 2026). They compared 806 GitHub projects that adopted Cursor with 1,380 similar projects that didn’t. Development sped up only in the first two months. Static analysis warnings rose by 30.3% and code complexity by 41.6%, and both stayed up. The paper finds they were major factors in the later slowdown. Duplicated lines didn’t change significantly.
  • Yue Liu and colleagues traced 302,600 AI-authored commits in 6,299 GitHub repositories for Debt Behind the AI Boom (2026), a preprint. More than 15% of commits from each of the five assistants studied introduced at least one issue. Code smells, patterns that suggest a deeper design problem, made up 89.3% of the issues, and 22.7% of the tracked issues were still there in the latest version.
  • GitClear’s AI Copilot Code Quality (2025) looked at 211 million changed lines from 2020 to 2024. Between 2021 and 2024, copy-pasted lines rose from 8.3% to 12.3% of changes, and lines GitClear ties to refactoring fell from 25% to under 10%. That’s observational data from a company that sells code analysis. It doesn’t show that AI caused the change.

Developers sometimes say so in the code. Abdullah Al Mujahid and Mia Mohammad Imran’s “TODO: Fix the Mess Gemini Created” (TechDebt 2026) looked at 6,540 code comments that mention an AI model. Of those, 81 also admit to debt, most often postponed testing, incomplete adaptation or code the developer didn’t fully understand. The paper calls such comments self-admitted technical debt and gives its usual keywords as TODO, FIXME, HACK and XXX. Those are the four terms the TypeScript and JavaScript packs’ no-warning-comments rule fails on.

Prickles’ Why page describes what you get from a model trained on decades of code written without best practice in mind: “Code that hangs together, and immediate technical debt.” The gates here treat an agent’s code the same way as anyone else’s. S2’s rule for agents, for example, tells the agent to run the linter after every edit, fix the function when the cap fails and never silence the rule. A longer list of smells, each with the rule that catches it, is in Code smells, and cleaning up an app built without rules has its own article.

If an agent writes code in your repository, run the same checks on its changes as on anyone’s, in CI and before each commit. Give it the rules in its instruction file too, so it can fix what the checks find. CLAUDE.md vs AGENTS.md covers which file each agent reads.