Guide

What is technical debt? A practitioner’s definition

Technical debt is the extra cost you pay on every future change because of a shortcut, an outdated decision, or a design that no longer fits the problem. Like financial debt, it has a principal (the work to fix it) and interest (the drag on every feature, fix and deploy until you do). Some of it is worth taking on.

I’ve inherited, created and paid down a lot of it over twenty years – from a PHP framework at Yola that had to carry 11 million users, to startup codebases that were never meant to survive their first round. This guide covers what the term actually means, where it came from, what it looks like in a real SaaS codebase, and when borrowing is the right call.

Where the term comes from

Ward Cunningham coined the metaphor in 1992, in an experience report about building WyCash, a portfolio management system. His point was narrower than how it’s used today: shipping code that reflects your current, incomplete understanding of the problem is fine, as long as you go back and refactor it once you understand more. The debt is the gap between what the code says and what you now know. Leave it unpaid and every change has to work around a model that’s wrong.

Over time the term widened to cover any code or infrastructure that makes change slower than it should be. That broader technical debt meaning is the one most teams use now, and it’s useful as long as you remember the core idea: debt is only debt if it charges interest. Ugly code nobody touches costs you almost nothing.

The technical debt quadrant

Martin Fowler’s technical debt quadrant is the clearest way I know to sort debt by how it was taken on. It has two axes: deliberate vs inadvertent (did you know you were taking a shortcut?) and reckless vs prudent (was it a sensible trade?).

  • Deliberate and prudent: “We’ll ship with a single Postgres database and no read replicas, and revisit when we pass a known load threshold.” You know the cost and you’ve chosen it.
  • Deliberate and reckless: “We don’t have time for tests.” You know it’s a shortcut and you’re pretending it won’t cost anything.
  • Inadvertent and reckless: A team that doesn’t know what a transaction boundary is writes billing code. Nobody chose the debt; it arrived with a skills gap.
  • Inadvertent and prudent: “Now we know how this domain actually works, we’d have modeled it differently.” This is Cunningham’s original case, and it’s unavoidable on any project where you learn as you build.

The quadrant matters because each kind needs a different response. Prudent debt needs a written note and a trigger for repayment. Reckless debt needs a change in how the team works – review, standards, seniority – or it keeps coming back however much you pay down.

Types of technical debt

Debt isn’t only in the application code. In SaaS codebases it tends to live in six places:

  • Code debt: duplicated logic, god objects, 2,000-line controllers, missing tests around the code that changes most.
  • Architecture debt: the wrong boundaries. Billing logic scattered across twelve models, or a premature split into services that all share one database.
  • Data debt: a schema that encodes an old understanding of the business. A users table that’s also the accounts table, nullable columns that are never null, money stored as floats.
  • Dependency debt: a framework two major versions behind, a language runtime past end of life, a library whose maintainer left. Interest compounds here because upgrades get harder the longer you wait.
  • Infrastructure debt: hand-built servers, no infrastructure as code, deploys that need one specific person.
  • Process debt: no CI, flaky tests everyone reruns, a release process that takes a day. This is the one that quietly multiplies all the others.

Technical debt examples from real SaaS codebases

Abstract definitions don’t help much when you’re looking at your own backlog. These patterns are common in growth-stage SaaS:

  • Single-tenant assumptions in a multi-tenant product. Tenant scoping done by hand in each query, so every new feature is a potential data leak. Fixing it later means touching every query. See multi-tenant SaaS architecture.
  • Plan logic hard-coded in the UI. “If plan == ‘pro’” checks across forty files. Changing pricing becomes an engineering project instead of a config change.
  • Webhooks processed inline. The Stripe webhook handler does all the work synchronously, isn’t idempotent, and times out under load, so retries create duplicate records.
  • A cron job that grew up. One nightly script now handles invoicing, emails and data exports, has no retries, and fails silently when one step breaks.
  • The prototype that went to production. A vibe-coded or agency-built MVP that won customers but has no tests, no migrations history and secrets in the repo.
  • Pinned forever. Rails 5 or Django 2 because one gem never got upgraded, which now blocks the Ruby or Python upgrade, which blocks security patches.

When technical debt is a good deal

Treat debt as a financing decision, not a moral failure. Borrowing makes sense when the return on shipping earlier is larger than the interest you’ll pay. That’s usually true before product–market fit, when most of the code you write will be thrown away anyway. Spending three weeks building a perfectly abstracted billing engine for a product nobody has bought yet is the worse mistake.

It stops being a good deal in three situations: when the debt sits in code that changes every week (high interest), when it sits in something that’s hard to reverse later (data model, tenancy, public API contracts), and when nobody wrote it down, so it can’t be repaid on purpose. My rule: take on debt freely in leaf code, carefully in core domain logic, and almost never in the data model.

How technical debt shows up in delivery and revenue

Founders rarely see debt directly. They see its symptoms, and those symptoms reach revenue faster than most engineering teams admit:

  • Estimates keep growing for features that sound simple, because every change touches five unrelated places.
  • Lead time for changes stretches from hours to weeks, and releases get batched because each deploy feels risky.
  • Incidents cluster around the same few modules, and on-call becomes a tax on your best engineers.
  • Onboarding takes months because the system only makes sense to the people who built it.
  • Sales loses deals because SSO, audit logs or data residency are “a big project” for structural reasons.
  • Good engineers leave. People don’t quit codebases, but they do quit the feeling of never being able to ship.

If several of these sound familiar, the developer productivity guide goes further into diagnosing a slow team, and the DORA metrics give you numbers to track it.

Measuring it without fooling yourself

Static analysis tools will give you a “debt score” in hours or days. Treat those numbers with suspicion; they count code smells, not interest. A messy module nobody touches scores badly and costs nothing.

A more honest measure combines churn (how often a file changes, from git history) with complexity or defect density. The files that are both complicated and frequently changed are your hotspots, and in my experience a handful of them account for most of the pain. Pair that with delivery metrics – lead time, change failure rate, time spent on unplanned work – and you have something you can show a board.

I’ve written the full playbook for doing this in how to reduce technical debt: measuring, prioritizing by business impact, and budgeting repayment without stopping feature work.

What to do next

Start a debt register: a short list of known debts, each with where it lives, what it costs you (in incidents, slowed features or lost deals), and what would trigger paying it down. Ten honest lines beat a 300-ticket Jira epic nobody reads.

If the debt has reached the point where the team is arguing about a rewrite, read rewrite vs refactor before anyone opens a new repo. And if you want a second pair of eyes, a code audit or a legacy modernization engagement is where I help teams decide what to fix first. Plenty of teams don’t need outside help for this; they need permission to spend a fifth of their time on it.

Questions

What is technical debt in simple terms?

Technical debt is the future cost of a shortcut or an outdated design in software. You save time now, then pay “interest” as slower development, more bugs and riskier releases until you fix it. Ward Cunningham coined the metaphor in 1992 to explain why teams need to refactor code as their understanding improves.

Who created the technical debt quadrant?

Martin Fowler. His quadrant classifies technical debt along two axes: deliberate vs inadvertent, and reckless vs prudent. It helps teams see that some debt is a sensible, conscious trade-off while other debt comes from carelessness or skills gaps, and each needs a different fix.

Is technical debt always bad?

No. Prudent, deliberate debt is often the right call, especially before product–market fit, when speed matters more than polish and much of the code will be replaced. It becomes a problem when it sits in frequently changed code, in hard-to-reverse decisions like the data model, or when nobody tracks it.

What are common technical debt examples?

Missing tests around critical code, duplicated business logic, outdated frameworks and runtimes, hard-coded plan or pricing logic, non-idempotent webhook handlers, manual deploys, and a database schema that no longer matches how the business works. The costly ones are those in code the team changes every week.

How do you explain technical debt to non-technical stakeholders?

Use the financial framing directly: every shortcut is a loan, and the interest is paid in slower features, more incidents and longer onboarding. Then attach numbers – lead time, incident count, deals lost to missing features – so the conversation is about business cost rather than code quality.

Not sure which debt is actually costing you?

I’ll spend a two-week sprint in your codebase, find the hotspots that matter, and fix the first one with your team. If it’s not useful, we stop.

GMT+2 · Full EU overlap · US East mornings

Related