Tech debt, short for technical debt, is the extra work every future change costs because of a shortcut taken earlier: the skipped fix is the principal, and the time each later change loses is the interest. Of the 26 apps whose code I audited in mid-2026, at least 23 had zero working automated tests.
What tech debt is: the definition of technical debt, and what counts
Technical debt counts anything in the code or its setup that makes the next change slower or riskier than it needs to be: missing tests, duplicated logic, outdated dependencies, missing documentation, a deploy only one person can run. A bug does not count, and neither does a missing feature or code someone simply dislikes.
Those 26 are the real applications I audited between June and July 2026, in three cohorts. I chose them for review, so the tests figure applies to those apps alone and is not a rate for apps in general. Keeping debt like that visible belongs with the other engineering standards for AI-assisted teams, the ones a small team can check without reading code.
Ward Cunningham coined the debt metaphor to explain the refactoring his team was doing on WyCash, a piece of financial software: if the program did not match what the team now understood, they would keep stumbling over the mismatch, and that slowdown was like paying interest on a loan. In Ward Cunningham’s own explanation of the metaphor he also says what it did not mean: in his view, a lot of bloggers confused it with writing code poorly and planning to do a good job later, and he is “never in favor of writing code poorly”. What he meant by technical debt was code written to match a partial understanding of the problem, repaid by refactoring once the team knows more.
In an app, the principal is the fix nobody made: the test never written, the rule pasted into a third file. The interest is what each later change pays for it, in extra hours of reading and in bugs in the copy nobody updated.
Technical debt is the formal term and tech debt is what engineers say out loud; the two mean the same thing. Technological debt and technology debt are other spellings of technical debt, not a separate kind. In an IT department, “IT debt” usually means something wider: aging systems, licenses nobody reviewed and servers nobody patched. In software development, technical debt is narrower, because its cost lands on the next code change.
To define tech debt for the rest of a team, ask of each item whether it slows down or endangers a change someone wants to make. Old code is not considered technical debt by itself; it becomes debt when a change has to work around it. A definition of tech debt that holds up in a planning meeting ties every item to a change on the roadmap.
The technical debt quadrant: deliberate or inadvertent, prudent or reckless
The technical debt quadrant is Martin Fowler’s two-by-two for sorting debt by how it was taken on: deliberately or inadvertently, and prudently or recklessly. Its value for an owner is the action each cell calls for: record the deliberate, prudent kind with a trigger to repay it, and stop the reckless kind before it merges.
Martin Fowler’s technical debt quadrant is a post from October 2009. His argument is that the useful line runs between prudent and reckless debt, not between debt and non-debt, and that deliberate and inadvertent debt differ too. He also holds that the odd-sounding fourth cell, prudent and inadvertent, is “not just common but inevitable for teams that are excellent designers”. His figure gives each cell of the tech debt quadrant a line someone might say. The first column below uses those labels; the other two columns are my own take on what each cell looks like in a small app and what an owner does about it.
| Cell, in Fowler’s labels | What it looks like in a small app | What to do about it |
|---|---|---|
| Deliberate and prudent: “We must ship now and deal with consequences” | A known shortcut to hit a launch date, such as a plan limit hard-coded in one place | Write it in the debt register with the event that triggers repayment |
| Deliberate and reckless: “We don’t have time for design” | A feature merged with no tests and copied logic because the week was full | Stop it at review, before it merges |
| Inadvertent and reckless: “What’s Layering?” | Database calls inside screen components, written by someone or something that did not know better | Catch it earlier with tests and linters |
| Inadvertent and prudent: “Now we know how we should have done it” | A data model that made sense at launch and fits badly now the product is clearer | Refactor when the better design is clear, one area at a time as changes reach it |
Other lists sort debt by where it sits: code debt, design debt, test debt, documentation debt or infrastructure debt; the quadrant sorts it by how it got there, which is what decides the action. How the quadrant reads for AI-written code, and the answer to “What are the four types of technical debt?”, sit in the FAQ of the article on technical debt in AI-generated code, which the section on why it matters links.
A technical debt example in an AI-built app, and four more
A technical debt example: a pricing rule written in three places. The interest is that every price change needs three edits, and missing one gives two customers two prices. Paying it off means one function that all three places call, with a test on it. The table below gives four more, each with its interest.
Take it as an illustrative case. An app built quickly by prompting gets a discount rule in the checkout, the invoice and the admin screen, each as its own piece of code. Nothing is wrong on launch day. The first price change edits two of the three, and a customer sees one total at checkout and another on the invoice.
The table puts that case in its second row and adds four more examples of tech debt that small apps carry. Its “interest” and “paying it off” columns are judgment calls of mine.
| The debt | The interest you pay | What paying it off looks like |
|---|---|---|
| No automated tests | Every change is checked by clicking through the app, and regressions reach customers first | Smoke tests on the flows that make or lose money, run on every change |
| The same business rule in three places | Each change needs every copy edited, and a missed copy is a bug | One function every caller uses, with a test on it |
| Untyped data crossing the API boundary | The backend renames a field and the frontend breaks in production without a warning | Shared types, or a schema checked on both sides |
| A framework pinned to an old version | Security fixes stop reaching the app, and the eventual upgrade jumps several versions at once | Upgrades in small steps, with the tests run between them |
| A deploy only one person can do | Releases wait for that person, and nobody else can roll back | A written, scripted deploy that someone else has run |
The first row is the gap the opener counted. In the same audits, 9 of the 26 apps ran a framework version with a publicly known, reachable RCE (remote code execution) or auth bypass, and the fix was often a one-line version bump. In agile terms, each of these technical debt examples becomes a backlog item with an owner and a test for done. Dependencies carry a second kind of debt, licenses nobody read, which is the job of license scanning.
Code rot: why code that nobody touched still breaks
Code rot is software that breaks without anyone changing it, because its surroundings moved: a runtime reached end of life, an API version was switched off, a certificate or root expired, a package got a security advisory. An app left alone for a year has taken on debt anyway, and a fresh install is the first test.
That is what code rot means in practice: the code stays the same while the world it runs in changes. Runtimes publish their support windows. On the Node.js release schedule, version 20 is marked EOL and versions 22 and 24 are LTS, and the page says production applications should only use Active LTS or Maintenance LTS releases. An app deployed on Node.js 20 and never touched since has moved from a supported runtime to an unsupported one without a single commit. Providers retire old API versions on their own timetable, and security advisories land on packages that were fine the year before.
A certificate shows the mechanism with dates attached. Let’s Encrypt’s note on the DST Root CA X3 expiration said DST Root CA X3, the older root certificate behind its cross-signature, would expire on September 30, 2021, so older devices that do not trust its own newer ISRG Root X1 would start getting certificate warnings on sites using Let’s Encrypt certificates, with one exception for older Android devices. For API providers it set two conditions: every client must trust ISRG Root X1, and OpenSSL clients must use version 1.1.0 or later, because in OpenSSL 1.0.x “a quirk in certificate verification” means that even clients that trust ISRG Root X1 would fail on the default chain. An update dated September 30, 2021 says the cross-sign expired as planned, and one dated February 5, 2024 says the Android compatibility cross-sign is close to expiring. Code nobody changed still breaks when something it depends on reaches an end date, and the owner who tracks those dates pays the debt on schedule instead of during an outage.
The signs an owner can see without reading code: a fresh install fails, the build prints warnings about deprecated packages, or a provider emails about an API version being switched off.
Maintainability: the property that debt wears down
Maintainability is how cheaply and safely software can be changed. ISO/IEC 25010 treats it as a quality characteristic with five parts: modularity, reusability, analysability, modifiability and testability. Tech debt is what wears it down, one shortcut at a time, until each change costs more than the last.
Outside software, maintainability means how easily equipment or a product can be repaired. In software it is about change. ISO/IEC 25010’s quality model lists nine quality characteristics and defines maintainability as “the degree of effectiveness and efficiency with which a product or system can be modified to improve it, correct it or adapt it to changes in environment, and in requirements”. In plain words, software maintainability is how much it costs to make the next change safely.
Per-file scores can look healthy while the app as a whole resists change, and that gap is the subject of why an AI-built app gets harder to change. The audit averages for maintainability against reliability sit in the AI-code article the next section points to.
Why it matters for an app you did not write
If you own an app someone else wrote, or one an AI assistant wrote for you, you hold a loan without the paperwork. Nobody wrote down which shortcuts were taken or why, so the debt has to be found before anyone can price it.
In practice the interest shows up as three things a founder can see without opening the code: estimates that keep growing for changes of the same size, fixes that break something in another part of the app, and one person who is the only one able to deploy. Each of those is a cost you already pay, whether or not anyone has called it debt.
The audit findings on maintainability against reliability in AI-built apps, and how to measure and pay down debt that an AI wrote, are in technical debt in AI-generated code. Taking over a codebase someone else wrote is its own job with its own order of work: legacy code takeover. If a raise is coming, the same debt matters to the investor’s technical reviewer, which raises the question of what investors look for in code.
How it works: measuring it, carrying it, and paying it down
The four parts below follow the order an owner meets the questions: how much debt there is, what to do about it, who schedules the work, and when taking on more is fine. The audit figures on this page come from my audits, with their denominators; every other fact comes from the source named beside it, and the rest is labeled as my own rule or view.
How to measure tech debt without reading the code
Tech debt has no single unit, so measure six proxies and watch the trend: tests that run and pass, type-check errors, lint warnings, outdated or vulnerable dependencies, deploys that needed a follow-up fix, and whether the README gets a clean checkout running. Tools report a debt ratio too, useful only compared with last month.
No single process or set of tools can measure technical debt in one unit, because debt is a cost to work that has not happened yet. SonarQube’s metric definitions show how technical debt is calculated in that tool: the debt is the sum of the estimated minutes to fix each maintainability issue, and the debt ratio divides it by the estimated cost to develop the code, with a default of 30 minutes per line that can be changed. Both figures rest on the tool’s own rules and estimates, so I’d use them for the trend in the same tool, never for a comparison with someone else’s app.
The six numbers below need no code reading, only a developer’s help for about an hour. I set the “bad value” column myself.
| The number | How to get it | What a bad value looks like |
|---|---|---|
| Automated tests that run and pass | The test command’s summary line | No tests, or failing tests that stay failing |
| Type-check errors | tsc --noEmit on the app’s own tsconfig, or tsc -b where the root tsconfig only holds references | Any errors on the main branch, or the check switched off in the build |
| Lint warnings | The project’s lint command | A count nobody reads, or one that grows every month |
| Outdated and vulnerable dependencies | npm outdated for versions, npm audit for known vulnerabilities (by default it needs a package-lock) | Known vulnerabilities left open, or packages several major versions behind |
| Deploys out of the last ten that needed a follow-up fix | The host’s deploy list: count the rollbacks and hotfixes | A follow-up fix after deploys as a habit, not an exception |
| A README that starts the app from a fresh checkout | Follow it from a fresh clone | It fails before the app starts, or only one person knows the missing step |
The fifth number is a small version of what DORA calls change fail rate, “the ratio of deployments that require immediate intervention following a deployment”; DORA’s definition of change failure rate adds that this is likely to mean a rollback or a hotfix.
To track tech debt, keep the six numbers as a dated row in the repository; I aim for a new row roughly once a month. A technical debt assessment by a person adds what the numbers cannot: which of the debt sits on the money, login and data paths, where a mistake costs the most. The tools and thresholds behind each number are in code quality checks. Repository signals particular to AI-written code, such as file size, churn and repeated names, belong to the AI-code article.
How to reduce tech debt, and how to manage what stays
Tech debt is reduced by paying the interest you are about to pay: before changing an area, add a test around it, delete what is dead there, and tighten its types, then make the change. Whatever you leave goes in a short register: the debt, where it sits, what it costs each change, and the trigger for fixing it.
That is my rule. It reduces technical debt where the next change is headed anyway, so the paydown and the feature share one round of testing. Each kind of debt has its own fix and its own method:
| The debt | What paying it off looks like | Where the method is |
|---|---|---|
| Missing tests on the critical paths | A few end-to-end smoke tests on signup, login, the core action and payment, run on every change | how to write end-to-end smoke tests |
| Dead and duplicated code | Delete what nothing calls; one shared function where there were copies | how to find unused code in a repo |
| Loose types | Strict mode switched on, errors fixed instead of suppressed | how to enable TypeScript strict mode |
| Outdated dependencies | Upgrades in small steps, tests run after each | The fourth number in the measuring table |
| A README nobody can follow | Rewritten from a clean clone until every step works | The sixth number in the measuring table |
To clean up a whole app rather than one area at a time, the order of the work matters, and how to clean up a vibe-coded app sets it out.
To manage technical debt you decide to keep, write it in a register with four columns: the debt, where it sits, the interest it charges each change, and the trigger for fixing it. I’d review that register about once a month. A register is how a small team can handle technical debt it cannot pay yet without forgetting it. Where debt is acceptable and where it is not is answered in the AI-code article’s FAQ.
Technical debt in Scrum and in product management: who owns the backlog line
Technical debt in Scrum is ordinary Product Backlog work: the Product Owner orders it with everything else, and the Definition of Done is the guard against adding more. Teams make room in three ways: a fixed share of each iteration, a debt item attached to each feature that touches the area, or a whole iteration spent on debt.
The Scrum Guide makes the Product Owner accountable for ordering Product Backlog items and calls the Product Backlog “the single source of work undertaken by the Scrum Team”. Its Definition of Done is “a formal description of the state of the Increment when it meets the quality measures required for the product”, and an item that does not meet it cannot be released. The guide does not use the term technical debt at all, so debt work is backlog work like any other.
The three ways to make room are my suggestion. A fixed share of every iteration is the easiest to keep going. A technical debt sprint, a whole Scrum iteration spent on debt, is the easiest to cancel when a deadline moves.
For a product manager, the case for debt work is made in the interest, not the principal: the days the last change in that area took, set against the days it would have taken in clean code. In project management usage, technical debt means the same thing with a budget line attached.
Technical debt at a startup: when borrowing is the right call
Technical debt at a startup is often the right loan: before anyone pays, learning fast beats tidy code, and much of what is written gets replaced. The kind never worth taking is the kind that can lose data, money or accounts. Start repaying at the first paying customer, the first engineer hired, or due diligence.
That is a judgment of mine, not a rule anyone enforces. Before the first payment most code is an experiment, and an experiment that gets thrown away never needs paying back. The exception is anything where a mistake lands on customers instead of on the next change: a missing backup, a payment flow nobody checked, access rules that let one account read another’s data. Those are not shortcuts worth taking at any stage.
How to check your own app: six numbers in an hour
A founder can size tech debt in an hour with six numbers: tests that ran and passed, type-check errors, lint warnings, outdated and vulnerable packages, follow-up fixes in the last ten deploys, and whether the README works from a fresh clone. Write them down with the date; a paydown worked when a number moved and nothing broke.
Start in the browser. On GitHub, open the repository’s Actions tab and check whether anything runs on a pull request. Then open the Security and quality tab and count the open Dependabot alerts. If there are none because alerts are off, a repository administrator can switch them on in the repository’s Settings, under the Security and quality section’s Advanced Security page.
The rest needs a terminal, or a developer for about an hour. The commands hold for a Node and TypeScript repository; other stacks use their own test, type and audit commands.
- 01 Run the test command. Record how many tests ran and how many passed. Evidence: the summary line.
- 02 Run the type check and record the error count:
tsc --noEmitagainst the app's own tsconfig, ortsc -bwhere the root tsconfig only lists references, as Vite's React and TypeScript template does. Evidence: the last line of output. - 03 Run the linter and record the warning count. Evidence: the summary line.
- 04 Run
npm outdatedandnpm audit. Record how many packages are outdated and how many vulnerabilities are reported; by defaultnpm auditneeds a package-lock file. Evidence: both summaries. - 05 From the host's deploy list, count how many of the last ten deploys needed a follow-up fix. Evidence: the dates of those deploys.
- 06 Follow the README from a clean clone and note the first step that fails. Evidence: that step, or a note that none failed.
On Vite’s template, the root tsconfig.json lists no files of its own, only references, so a root tsc --noEmit has nothing to check; tsc -b follows the references to the configs that hold the app’s code.
Write the six numbers in one dated row and repeat on the monthly rhythm from the measuring section. Nothing broke means the smoke tests still pass after the paydown, and a number that moved is the evidence the work was worth doing.
Where the sprint fits
The Production Hardening Sprint’s area 10, code health and AI development guardrails, covers the kinds of debt on this page, each in the published wording: 10.1, “Remove dead code and consolidate duplicated logic while preserving required behavior” ; 10.3, “Define types at every API boundary and share contracts between frontend and backend using the stack’s appropriate tooling” ; 10.4, “Organize the codebase consistently and explain its layout in the README” ; 10.5, “Automate smoke tests for signup, login, the core product action, and payment flows”, which we verify this way: “Run the suite in CI and demonstrate that an intentional regression fails it” ; and 10.6, “Enable TypeScript strict mode and resolve errors in TypeScript applications, with an equivalent strict-checking approach for other supported stacks” . Outside this sprint: building new product features or modules, completing unfinished core features or business workflows, and rebuilding core functionality that does not yet perform its intended job; new features and completing unfinished core workflows are separate work. The app’s current framework and hosting setup are the starting point; components are refactored or replaced where the production work requires it. Every deliverable is listed in the published scope.
Common questions about technical debt and code rot
Is tech debt a bug?
No. A bug is behavior that is wrong now; tech debt is code that works today and makes the next change slower or riskier. Debt raises the odds of bugs, and a bug that costs too much to fix and gets worked around can turn into debt.
What is another name for technical debt?
Tech debt is the usual short form, and IBM’s and Confluent’s guides also call it code debt, with IBM adding design debt. “Technological debt” and “technology debt” are spelling variants, not separate terms.
Can code rot be fixed?
Yes. Upgrade the runtime and the dependencies in small steps behind smoke tests, then keep automated dependency updates switched on; on GitHub, Dependabot version updates are “automated pull requests that keep your dependencies updated, even when they don’t have any vulnerabilities”. Put the runtime’s support status from its release schedule in the debt register, so the next end date is planned work.
What is the difference between reliability and maintainability?
In ISO/IEC 25010 terms, reliability is the “degree to which a system, product or component performs specified functions under specified conditions for a specified period of time”, while maintainability is how effectively and efficiently it can be modified. An app can be easy to change and still fail often, and an app that rarely fails can be very hard to change; the audit averages for both are in the AI-code article.
Owning an app means being able to run it, change it and recover it without guessing. The sprint below leaves you with the runbooks and documentation to do that.
Built your app with AI? Get it ready for what comes next.
Whether you’re preparing for a launch, client handover, internal rollout, or technical review, we help strengthen the security and reliability of the app you already have.
Our engineers implement and verify the production controls inside your existing codebase, then hand over the changes, test results, and operating instructions.
Explore the Sprint →No commitment to proceed.
10 working days · Your existing codebase · Documented verification