Abstract code structure with tangled lines and glowing nodes, representing complexity in software engineering

Technical debt has been kicking around software engineering since Ward Cunningham first compared it to financial debt back in 1992. The original idea was clean: you take a shortcut now, and you pay for it later, with interest. It gave devs and managers a shared language for a genuine headache. But after three decades, the metaphor has hardened into something that causes more trouble than it solves. It oversimplifies, misleads, and straight-up warps how we think about code quality and maintenance.

I’ve spent twenty years building systems in C++, Java, Rust, and a scattering of other languages. I’ve seen codebases the metaphor helped, and a lot more where it just handed people an excuse. The problem isn’t that the concept is flat wrong. It’s that the financial analogy squashes a multidimensional engineering mess into a single number—dollars, euros, story points—and nudges us to treat code rot like a balance sheet line item with scheduled payments. That’s not how software behaves.

The Origins of a Flawed Comparison

Cunningham’s original insight was solid. Ship code you know isn’t quite right, and you’re borrowing against tomorrow’s productivity. You get faster delivery today, but you owe a refactoring session down the road. The snag is, real financial debt has clear properties that software entropy simply doesn’t. Debt has a principal, a known interest rate, a creditor with rights, and usually a fixed term. Borrow €10,000 at 5% over three years, and you can calculate exactly what you’ll owe in month 24. Technical debt has none of that. The cost of a sketchy module might sit flat for years, then explode when a new integration forces a rewrite—the “interest” doesn’t tick, it spikes. Or it might never matter because the feature gets axed. The metaphor promises predictability where there is none.

It also smuggles in a moral undertone. Financial debt is something you’re supposed to feel guilty about. That’s handy for personal budgeting, but in engineering, it leads to teams labeling any imperfect code as debt and then carrying around shame. I’ve watched junior developers tie themselves in knots over a helper function that isn’t purely functional because someone told them it was “accruing interest.” That’s nuts. We don’t need guilt; we need sharp reasoning about trade-offs.

What the Metaphor Hides: Code as a Liability Structure

A developer staring at multiple monitors showing code and debugging tools, reflecting the real burden of software maintenance

Let’s get concrete. Picture a function handling user authentication in a Python web app. It checks credentials against a database, hashes passwords, and sets session tokens. It also logs directly to a file instead of using the standard logging module, and it catches exceptions with a bare except: block. A financial-debt thinker would tally these as “debt items” and maybe assign severity scores. But that misses the actual engineering reality.

Here’s a stripped-down version of what that might look like:

def authenticate(username, password):
    try:
        user = db.query("SELECT * FROM users WHERE username = ?", username)
        if not user or not check_hash(password, user.hash):
            return None
        token = generate_token(user.id)
        with open("/var/log/auth.log", "a") as f:
            f.write(f"{username} logged in\n")
        return token
    except:
        return None

The bare except is a risk because it silences every possible exception—KeyboardInterrupt, MemoryError, you name it. The direct file write bypasses log rotation, aggregation, and format consistency. These aren’t “debts” with a principal and interest; they’re latent failure modes with different triggers and blast radii. The bare except might cause silent data loss if the database is unreachable in a particular way. The logging shortcut might only matter when your ops team is debugging a production outage at 3 a.m. and can’t find the auth failures. One is a reliability risk, the other an observability risk. Calling both “debt” dumps them into a bucket that invites identical treatment—usually a half-hearted refactoring sprint that never materializes.

Compositional Complexity Doesn’t Compound Like Interest

Financial interest compounds. Ignore it, and it grows. Software complexity doesn’t always follow that script. Sometimes a messy module stays isolated and causes zero downstream effects. Other times, two individually tolerable shortcuts interact in a way that creates a combinatorial explosion of edge cases. That’s not interest; that’s a system dynamics problem. The financial metaphor has no room for the fact that bad code in a library with five callers is a different beast than bad code in a library with fifty callers. Debt, in the bank sense, doesn’t care how many people use the money.

Take a C++ codebase I worked on years ago. We had a class that managed hardware registers for an embedded device. It used raw pointers and manual memory management because the team was in a hurry. Classic “debt.” But the real cost only surfaced when we needed to make the driver thread-safe. The pointer aliasing and lack of ownership semantics meant we had to audit every call site across thirty files to understand who held references and for how long. The “interest” wasn’t a steady tick; it was a cliff we hit when requirements changed. If we’d modeled it as debt, we might have paid down some of it during a quiet period, but the thread-safety requirement was the actual trigger, and no amount of metaphorical payments would have dodged the audit unless we’d fixed the ownership model specifically.

The Social Damage: Debt as a Weapon

Inside organizations, the technical debt metaphor has turned into a political tool. Product managers learn to ask, “How much debt are we taking on?” as if the answer is a number. Engineers fire back with estimates in story points or developer-days, which then get discounted against future velocity. You end up with a negotiation dynamic detached from engineering reality. You’re not trading off “debt” against features; you’re trading off increased probability of specific failure modes against time-to-market for a feature that may or may not pan out. Those probabilities don’t add up neatly the way dollars do.

I’ve sat in meetings where an architect argued against a quick fix by saying it would add €50,000 of technical debt. Where does that number even come from? It’s a guess wearing the clothes of financial authority. The real argument should have been: “This fix introduces a tight coupling between the payment module and the user service, which means any change to user data fields will break payment processing in ways our integration tests don’t cover. The expected cost of that breakage, given our release cadence and the planned user service refactor, is high.” That’s a specific, testable claim. The debt number is a handwave.

The “Strategic Debt” Trap

Then there’s the notion of strategic technical debt—supposedly debt you take on deliberately with a clear repayment plan. In finance, strategic debt means something like a mortgage on a factory that will generate revenue to cover the payments. In software, it usually means we’re shipping this hack now and promising to fix it in Q3. Q3 arrives, and there’s a new VP who wants to pivot the roadmap. The “strategic” part evaporates because the knowledge of what needed fixing lived in the heads of two engineers who left in Q2. The factory metaphor doesn’t hold; software decays through personnel churn and context loss, not through a predictable wear-and-tear schedule.

A worn-out industrial control panel with tangled wires, symbolizing legacy systems that defy simple debt calculations

Towards a Better Vocabulary

If we ditch the debt metaphor, what do we put in its place? I’m not arguing for zero language. We need ways to talk about code quality that are precise, actionable, and free of moral baggage. Here are a few distinctions I’ve found useful in the field.

Contingent Liabilities vs. Friction

A contingent liability is a piece of code that will cause a specific failure under a specific condition. That bare except block above is a contingent liability: it will swallow an exception you need to see if that exception fires during authentication. You can describe the trigger condition, the failure mode, and the blast radius. You can test for it. You can decide whether to accept the risk based on how likely the trigger is and how bad the failure would be. That’s an engineering call, not an accounting one.

Friction is code that makes future changes harder without necessarily causing failures. A function that takes seven positional parameters when a configuration object would be clearer is friction. It won’t crash, but every new feature touching that function forces you to remember the parameter order and meaning. Friction accumulates, but it doesn’t compound like interest. It’s more like silt in a pipe: it reduces flow, and if you ignore it long enough you might need a bigger pump, but the relationship is linear with usage, not exponential over time.

Integrity Constraints and Invariants

Some of what we call debt is actually violated invariants. An invariant is a property that must hold for the system to be correct. If your user service assumes usernames are unique but your database schema doesn’t enforce that, you’ve got a violated invariant. That’s not debt; it’s a correctness bug waiting to bite. The fix is to enforce the uniqueness constraint either in the database or in the application logic with proper locking. Calling it debt hides the fact that the system is currently incorrect in a way that could produce wrong results, not just slow development.

Here’s an example. Suppose you have a Rust struct that should never hold a negative balance, but you’ve used an i64 instead of a newtype with a constructor that checks:

pub struct Account {
    pub balance: i64,  // Invariant: should be >= 0
}

// Somewhere else in the code:
fn withdraw(account: &mut Account, amount: u64) -> Result<(), Error> {
    account.balance -= amount as i64;
    Ok(())
}

If a bug elsewhere sets balance to -100, the withdraw function happily makes it more negative. The invariant is broken, but the code compiles and runs. A financial-debt mindset might tag this as “technical debt: missing validation” and prioritize it alongside a dozen other items. But it’s not debt; it’s a time bomb. The distinction matters because time bombs need immediate attention, while friction can be managed incrementally.

Why the Metaphor Persists

Given all this, why do we keep using it? Partly it’s institutional. The finance analogy gives engineers a way to talk to business stakeholders in a language they get. “We have $2 million in technical debt” grabs attention in a boardroom in a way that “We have 47 instances of violated invariants and significant friction in the payment module” does not. I understand that. But it’s a trap. By translating engineering concerns into a lousy financial analogy, we hand the frame to people who will then treat the problem with financial tools: budgeting, amortization, write-offs. You can’t write off a race condition. You can’t amortize a missing database index that’s causing full table scans on every login.

The metaphor also sticks because it offers the illusion of control. If technical debt is just like financial debt, then we can manage it with the same spreadsheets and dashboards we use for everything else. We can track it in JIRA, assign it story points, and feel productive. But the stuff that actually kills software projects—architectural erosion, undocumented assumptions, implicit coupling—doesn’t show up on those dashboards. They’re not debts; they’re structural properties of the system that the debt metaphor actively hides.

Practical Steps Away from the Metaphor

If you want to move your team away from debt-thinking, start by changing the questions you ask in code review and planning. Instead of “How much debt does this add?”, ask:

  • What invariants does this code depend on, and where are they enforced?
  • What failure modes does this introduce, and how would we detect them in production?
  • What future changes would become significantly harder because of this code’s structure?
  • What knowledge about this system is only in someone’s head right now?

These questions don’t have simple numerical answers, and that’s the point. They force you to think about the actual properties of the software instead of swapping in a metaphor and moving on.

When you do need to communicate with non-engineers, use concrete scenarios. “If we ship this without fixing the error handling, and the payment gateway times out during the Black Friday peak, we’ll lose roughly €50,000 in revenue per hour until we can deploy a patch. The patch will take at least four hours because it requires a database migration.” That’s a statement a CFO can evaluate. It’s not a debt figure; it’s a risk scenario with a cost estimate. It’s honest about the uncertainty and specific about the mechanism.

FAQ

Is all technical debt bad?

The question itself is a product of the metaphor. If you stop calling it debt, you can ask better questions. A quick-and-dirty script that parses a one-off data file and will never be run again isn’t “good debt” or “bad debt”; it’s just a script with a limited lifespan and no maintenance requirement. A complex subsystem with weak encapsulation isn’t “debt” at all; it’s a module with high coupling that will make certain kinds of changes expensive. Whether that matters depends entirely on whether you plan to make those kinds of changes. The moral categories of good and bad debt are distractions.

What should I say instead of “technical debt” in a sprint retrospective?

Describe the specific property that worries you. “The authentication module has a bare except that could swallow database connection errors silently” is a concrete observation. “We have a lot of tech debt in the auth module” is a feeling. The first one can go into the backlog as an actionable item with clear acceptance criteria. The second one generates a vague task that nobody wants to pick up because it’s undefined.

If we drop the metaphor, how do we prioritize quality work against features?

You prioritize based on risk and cost of delay, the same way you prioritize features. A violated invariant that could cause data corruption goes to the top because the potential loss is high. Friction in a module you’re not planning to touch for two quarters goes to the bottom. This is ordinary engineering risk management. The debt metaphor doesn’t help with this; it just gives you a pseudo-quantitative way to feel like you’ve done the analysis when you haven’t.

Isn’t the debt metaphor just a communication tool? Why take it so literally?

Because metaphors shape thought. When you use a financial metaphor, you import financial assumptions: that obligations are fungible, that interest is predictable, that you can pay down principal on a schedule. These assumptions are wrong for software, and they lead to bad decisions. A communication tool that systematically misleads its users isn’t a tool; it’s a liability in its own right. We can do better.

The technical debt metaphor served a purpose when it gave engineers a bridge to business language. But we’ve been on that bridge for thirty years, and it’s time to admit it leads to the wrong place. Software quality isn’t an accounting problem. It’s an engineering problem, and it deserves engineering language.