"This quarter we're going to focus on paying down technical debt."

When the tech lead says this, most of the room nods. Makes sense — those notoriously bad modules in the codebase have made everyone suffer. Three months later, plenty of refactoring was done, the code is indeed cleaner. But only one of the three most important business requirements shipped.

Was it wrong? Not necessarily — the refactoring might have had real value. But more likely, you paid down debt that shouldn't have been prioritized. That code, while ugly, didn't block the business, didn't produce errors, didn't impede debugging. You paid it off and felt good. The most dangerous debt is still there, unnoticed by anyone.

"Paying down technical debt" can't be a slogan. What to pay, when to pay, what happens if you don't — these three questions demand more judgment than "pay it all."

What Technical Debt Isn't

First, what technical debt isn't. Because most "paydown" energy gets spent on things that aren't debt.

Technical debt isn't "code style you don't like." Whether a project uses tabs or spaces, classes or functions, whether variable names are "elegant" enough — these are preferences, not debt. Preferences don't affect system behavior; changing them doesn't make the system better.

Technical debt isn't "old tech stacks." A service running on Java 8 that works stably, has low change requirements, and is familiar to the team — that's not debt. It's just not using the latest thing. Upgrading to Java 21 costs weeks of effort and uncertain compatibility issues; the benefit is "we're on the latest version now." That's not paying debt — that's chasing the new.

Technical debt isn't "code someone else wrote." Code you don't understand isn't necessarily problematic code. It might be a decision you'd have made at that point in time. Try understanding the context before rushing to rewrite it.

Technical debt is something that affects your ability to move fast or the system's correctness. More precisely: it's a structural problem where today's decisions (or past ones) reduce future change velocity, increase fix costs, or introduce correctness risks. The difference from "ugly code": ugly code doesn't necessarily need fixing; technical debt, if left unfixed, will bite you at some point.

One more thing worth saying: technical debt isn't necessarily because the original code was written wrong. Often it's because your understanding of the problem has changed. A design from six months ago based on then-current understanding — now the business has changed, the problem has changed, and the original design has become debt. This isn't "was written wrong" — it's "information has updated." This distinction isn't wordplay — it affects how you treat the person who wrote that code. If it's "written wrong," the attribution is to human ability. If it's "information updated," the attribution is to the inevitability of time and change. The latter carries less psychological weight and is closer to the truth.

Three Dimensions

For prioritizing technical debt, I evaluate along three dimensions:

  1. Evolution velocity — Is this debt currently slowing delivery of your most important business requirements?
  2. Correctness — Has this debt already caused or is it about to cause data errors?
  3. Observability — Does this debt leave you blind when production breaks?

Not all dimensions apply to every debt. But most debt worth paying scores high on at least one.

Evolution Velocity

Changing one business logic requires touching 15 files — that's debt. Not because 15 is a lot, but because the cost of changing one business concept is multiplied 15-fold. A "product listing" requirement changes ProductService, InventoryService, SearchService, CacheService, NotificationService — each with a few lines of product-status-related code, and nobody remembers all of them. Miss one, and production is inconsistent.

Adding any new feature requires first understanding a 3,000-line function — that's debt. Not because 3,000 lines is a lot (though it is), but because comprehension cost becomes a fixed overhead for doing anything. A new hire spends a week just reading this function. That week isn't learning the business — it's learning someone's coding habits and three years of accumulated hacks.

But frequency matters too. If "that mess of code" is ugly but only touched once a year, its priority is far lower than ugly code that's modified daily. Debt interest is proportional to touch frequency. Something touched once a year — refactoring it pays off at most once annually. Something touched daily — refactoring pays off every day. Not saying the once-a-year code never gets refactored — saying it shouldn't be prioritized today.

An easily overlooked angle: some code is slow to change not because the structure is bad, but because the business itself is complex. Misidentifying "complex business logic" as "code debt" and refactoring it, only to find it's just as slow afterward — because the slowness wasn't code structure, it was the inherent number of business rules. This "refactoring" didn't pay any debt; it just rewrote the code.

Correctness

In most business systems, this dimension should be the highest priority.

State machine has holes — data can enter illegal states. This kind of debt should be prioritized. Not "next release" — schedule it into the current iteration or the nearest fix window. Once data enters an illegal state, all downstream logic depending on that data runs on wrong premises. You think it's just a small hole, but data has inertia — bad data generates more bad data until one day you realize the entire table's state distribution is untrustworthy.

Unclear transaction boundaries — data can become inconsistent. Write A succeeds, write B fails, A doesn't roll back, data is dirty. Not paying this debt doesn't mean "it'll be slower later" — it means the problem will eventually escalate from code-level to data-repair-level. Fixing data is usually far more expensive than fixing bugs.

A data error has already happened once in production and was manually recovered — this should be addressed before it repeats. The first time is a warning. The second time is an incident. The third time is systemic distrust. If business stakeholders are already saying "your data can't be trusted," you owe not technical debt but trust debt.

The same applies to security — plaintext storage of sensitive information, SQL concatenation, unauthenticated internal APIs exposed publicly. These aren't just "technical debt" — they're explicit risk items. Whether or not they're called "technical debt" doesn't matter, but they must appear on the priority list, near the top.

The correctness dimension test is simple: if this debt isn't paid, will data go dirty? Will the system become unavailable at some point? If yes, it's the highest priority.

Observability

When something breaks, do you find out from monitoring or from user complaints?

If the latter, you owe observability debt. Not that you lack logs — you might have extensive logging. But there's no structured tracing on critical paths, no ability to correlate requests across upstream and downstream, logs "always missing the key field" during troubleshooting. Every debugging session starts not with checking monitoring but with "let me guess where the problem might be." Guess right — lucky. Guess wrong — hours of manual investigation.

Observability debt is insidious: it doesn't affect normal feature delivery. The happy path runs fine, tests pass, deployments are smooth. But its impact appears when you need speed most — production is broken, users are complaining, you need to identify the cause in 10 minutes, but it takes 40. The extra 30 minutes isn't because your skills are lacking — it's because the system didn't give you enough information.

Another often-overlooked dimension: observability debt affects your judgment of other debt. You don't know how severe a debt is because you lack data. You don't know if a module's failure rate is 0.1% or 5% because there's no instrumentation on that chain. Your "prioritization" might be blind from the start — because you can only judge by feel, not by data.

This is higher priority than a lot of "ugly code" debt. Ugly code makes changes slower but doesn't affect response speed when things break. Missing observability makes you blind when problems happen. Which is more dangerous shouldn't require much thought.

When It's Worth Borrowing

Not all debt should be avoided. Consciously incurred debt and unknowingly incurred debt are two different things.

To validate a business model, you launched quickly — intentionally simplified the design, skipped abstraction layers, wrote everything in one function. If this got the product to market two months earlier and yielded critical market feedback, the debt was worth incurring.

But this "strategic debt" has two prerequisites.

First: you know what you owe. Not "I know this is written poorly" — a vague awareness. A list: where simplifications were made, which edge cases weren't handled, which scenarios aren't covered. Without this list, "pay it later" means "never pay it."

Second: you have a paydown window and plan. Not "when we have time" — there's never time. But "before X, this must be paid, otherwise Y will happen." For example: "Before we reach 10,000 users, the payment API must be made idempotent, otherwise the risk of duplicate charges is unacceptable." Debt with a deadline might actually get paid.

Without these two prerequisites, "let's do it this way for now and deal with it later" isn't strategic debt — it's inertia. The difference from strategic debt isn't in the outcome (both incur debt) but in whether you made a conscious judgment when incurring it.

Communicating with Management

Many technical teams clash with business stakeholders over debt, not because the debt is unimportant, but because the communication approach is wrong.

"This code is terrible and needs refactoring" — management hears not "needs refactoring" but "engineers want to spend time writing code they find pretty." This translation is lossy, but engineers often don't realize it.

Try: "This module's current state means every related business logic change takes an extra two days, and the bug rate after release is 30% higher than other modules. If we spend one week refactoring, delivery time for every subsequent related requirement shrinks by two days." Two days times the number of requirements per quarter gives you saved person-days. One week of refactoring costs person-days. Compare the two — ROI emerges.

Translate debt into business impact. Not "ugly code" but "this debt costs us two extra days per month handling production issues." Not "architecture needs optimization" but "if this service goes down, recovery time doubles because we lack the observability to locate problems quickly."

Give options, not just demands. A: spend a week refactoring, accelerate subsequent delivery. B: don't refactor but add monitoring, at least problems can be quickly located. C: do nothing, maintain status quo. Letting management choose between concrete options is far more effective than asking them to decide on the abstract question of "should we pay down technical debt."

Not Zero Technical Debt

Your goal isn't a zero-debt codebase.

No production system in operation has zero technical debt. If your system is running, changing, and serving users, you're constantly making tradeoffs — tradeoffs create debt.

The goal is that debt is managed. You know which debts exist. You know which are high-priority. You have a rhythm that isn't "when we have time" but "must be addressed before X." You know which debts are slowing you down, which threaten correctness, and which leave you blind when things break.

The rest can wait.

Not all debt must be paid. But all debt must be seen.