Tech Debt Isn't the Enemy. Treating It Like an Obligation Is.

6 min read

Tech debt is not a moral failing. It's a liability on your balance sheet. And like any liability, the only rational way to evaluate it is by weighing the cost to resolve it against the risk of leaving it alone.

Most teams never actually do that evaluation. They either treat messy code as an emergency that demands immediate refactoring, burning engineer-hours on cleanup that delivers no user value, or they treat the whole concept as imaginary and let it pile up until something breaks in production. I've been on teams that made each of those mistakes, and the cost was real both times.

The "let it rot" argument

A LinkedIn post made the rounds recently that opened with a story about a CTO who spent three sprints fixing tech debt on a feature that got deleted. Users hated it, nobody used it, leadership killed it. Three sprints of refactoring, beautiful code, perfect architecture, all for something that no longer exists. The post's conclusion was that the smartest founders let tech debt rot on purpose, because roughly half of it is attached to features that will change or die before the debt matters.

I commented publicly on it, because I think the advice is genuinely dangerous when you apply it broadly. Some tech debt puts the company at risk if left unresolved. Tech debt is more than messy code. It's a liability that has to be evaluated based on the potential benefit to the business and the cost to change, just like any other liability. Sometimes it's worth leaving alone. And sometimes, truth be told, it'll make your company fall apart.

The author responded that he agreed critical debt should never be ignored, but that a lot of teams spend weeks fixing debt on features that get deleted the next quarter. And that's fair. The CTO-burns-three-sprints-on-a-doomed-feature story is real. I've seen versions of it.

But here's what I said back, and it's the core of why I'm writing this post. In my 20+ years across roughly a dozen global corporations, I've seen thousands of developer hours go toward paying down tech debt. Thousands. And with the most liberal estimate I can give, maybe 50 of those hours went to improving a feature that got deleted before it provided substantial benefit to the company. Fifty out of thousands. And even then, even in those cases, what if the feature gets deleted next quarter but those changes earned the company a million dollars while it was still live? You can not just handwave that away. The whole take is very black-and-white about a topic that is about as nuanced as it gets.

The deeper problem with the post is that it treated tech debt as one thing. It isn't.

Three different problems wearing the same name

There's friction debt. Inconsistent patterns, duplicated logic, poor naming, missing tests. It costs you a little bit every sprint. It makes onboarding slower, code reviews longer, and context-switching more expensive. It compounds over time, but it rarely detonates. You don't schedule sprints to address this kind of debt. You address it incrementally, cleaning up what's adjacent when you're already in that code for a feature or a bug fix. No dedicated sprint. No product stoppage. Just a rule that says you leave the code a little better than you found it.

There's risk debt. Security vulnerabilities. Deprecated dependencies. No error handling in critical paths. An untested payment flow. This kind of debt has a probability-weighted cost attached to it: a breach, an outage, a regulatory violation. And leaving it alone because "the feature might get cut" is exactly how companies end up in incident reviews explaining something that was known and deferred. I have watched that happen. It is not theoretical.

And then there's scale debt. An architecture that works at 1,000 users but degrades at 100,000. A data model that made sense two years ago but now requires workarounds for every new feature you build on top of it. You don't pay this down proactively, but you need to know it exists. You need a plan on the shelf for when growth forces the issue, because by the time the symptoms are visible to leadership, you're already behind.

The mistake, and I see it constantly, is treating all three the same way. A team that runs dedicated "tech debt sprints" is probably over-investing in friction debt. A team that ignores everything is accumulating risk debt they can not see. The evaluation has to be specific to what kind of problem you're actually looking at.

Nelnet and SAMi

Two of my consulting engagements illustrate this well, and what makes them useful is that they involved the same category of debt with completely different outcomes.

At Nelnet, I inherited a massive Objective-C codebase for a community outreach app. The debt was friction-class. The app worked. It wasn't causing outages. But every engineer who touched it slowed down, and onboarding new developers meant explaining a parallel set of patterns that existed nowhere else in the modern iOS ecosystem. So we didn't do a big-bang migration. We created one simple rule: any time someone needed to modify an Objective-C file for a feature or bug fix, they migrated that file to Swift as part of the same PR. That's it. The debt paid itself down as a byproduct of work we were already doing. No roadmap disruption. No "refactoring sprint." Just steady, incremental improvement attached to real product work.

At SAMi, the calculus was completely different. The legacy Objective-C codebase wasn't just slowing people down. It was actively preventing the product from doing what the business needed next. The architecture could not support the features on the roadmap, and the cost of building workarounds was compounding every sprint. That warranted a full rewrite into SwiftUI, scoped and planned and executed as its own initiative with a clear business case behind it. It wasn't optional optimization. It was a prerequisite for the next phase of the product.

Same category of debt on paper. Objective-C in a Swift world. Completely different responses. Because the cost of delay and the type of risk were different in each case, and that distinction is what actually matters when you're making these decisions.

How I evaluate it

When I'm deciding whether to address tech debt on a team I'm leading or advising, the question is never "is this code messy?" The question is "what does it cost to leave this alone, and does that cost justify what we'd spend to fix it right now?"

That means thinking about whether the feature this debt lives in is growing, stable, or already being questioned internally. Debt on a feature that's generating revenue and getting active development gets addressed. Debt on something that hasn't moved a metric in two quarters stays in the backlog until something changes. That's not neglect. That's prioritization.

It means being honest about what kind of debt you're looking at. Friction gets routed into existing work. Risk gets escalated. Scale debt gets a plan.

It means accounting for opportunity cost explicitly. Three sprints on a refactor is a real bet. What doesn't ship during that time? Is the expected return from the cleanup actually higher than the value of the features you'd build instead? Sometimes it is. Sometimes it clearly is not, and the team needs to hear that directly rather than letting the loudest voice in the room set the priority.

And it means asking, honestly, what happens if you wait another quarter. If the answer is "developers will grumble a bit," that's not a crisis. If the answer is "we could have a data integrity incident before the next audit," that changes the conversation immediately.

The teams I've seen handle this well aren't the ones with the best engineering practices. They're the ones who make this evaluation out loud, together, and treat it as the judgment call it actually is rather than defaulting to whoever feels most strongly about either shipping or refactoring.