← Back to list

How I Decide Which Tech Debt to Kill and Which to Protect

After years of leading engineering teams, I stopped treating all tech debt the same. Here’s the actual framework I use.

Milan Dadhaniya in Simform Engineering · 2026-06-25 05:50 · 76 claps · 7.5 min read
#tech-debt-prioritization #when-to-refactor-code #legacy-code-in-prod #tech-debt-vs-features #engineering-tech-debt
Open on Medium ↗

How I Decide Which Tech Debt to Kill and Which to Protect

After years of leading engineering teams, I stopped treating all tech debt the same. Here’s the actual framework I use.

Every lead engineer has been in this meeting.

Someone from product wants a new feature by end of quarter. Your backend has a data model that was clearly designed on a Friday afternoon in 2021. The authentication module is a tower of duct tape that somehow hasn’t fallen over. And somewhere in the backlog, there are 47 tickets tagged “tech debt” that haven’t been touched in eight months.

The question is never “do we have tech debt?” The question is always “which of this debt actually matters right now, and which can safely stay buried?”

For a long time I answered that question with instinct. Senior engineers would flag something, I’d agree it was messy, it would go into the backlog, and the cycle would continue. Nothing systemic. Nothing defensible when product asked why we were spending sprint capacity cleaning up old code instead of shipping.

Then we had an incident that forced me to get serious about it.

The Incident That Changed My Thinking

We were scaling a Node service that had been humming along fine at moderate traffic. As load increased, we started seeing intermittent failures that were nearly impossible to reproduce locally. It took three engineers two weeks to trace it back to a connection pooling pattern that had been written years earlier — technically functional, completely unmaintained, and sitting on an assumption about concurrency that was no longer true at our scale.

Nobody had flagged it as debt. It wasn’t messy-looking code. It just had a hidden load-bearing assumption that nobody remembered making, and nobody had documented.

That incident taught me the most important thing I know about tech debt: the danger isn’t always in the ugly code. It’s in the load-bearing assumptions that nobody wrote down.

After that, I started thinking about debt completely differently.

The Two Questions That Actually Matter

Most frameworks for evaluating tech debt ask you to score it on “effort to fix” vs “impact if fixed.” That’s fine for prioritization once you’ve already decided something needs fixing. It doesn’t help you answer the harder question: should this be fixed at all right now, or should we protect it?

The two questions I actually ask are:

1. What is this debt blocking? Not in the abstract — specifically. Is it blocking a feature we’re shipping in the next two quarters? Is it blocking hiring, because no engineer wants to touch it? Is it blocking reliability, because it degrades under load? Or is it sitting in a stable part of the system that nobody is touching and nobody needs to touch?

2. What happens when this breaks — not if? Every piece of debt breaks eventually. The question is what the failure mode looks like. Does it fail loudly with an error you can trace immediately? Does it fail silently in a way that corrupts data? Does it cascade across services? Does it require the one engineer who wrote it to fix it, or can anyone with context debug it in under an hour?

These two questions together tell me almost everything I need to know. Debt that blocks active work and fails catastrophically when it breaks gets killed. Debt that sits dormant and fails loudly and cleanly when it eventually breaks? That can wait — and sometimes, it can wait forever.

The Four Categories I Actually Use

Over time I’ve settled on four buckets. I don’t use a scoring matrix or a spreadsheet. I use these four words, and they’re enough.

Kill — fix it now, in this quarter, full stop.

This is debt that is actively in the critical path of something the team is building, or that carries a failure mode that would be catastrophic and silent. The Node connection pooling issue above was Kill category — not because it looked bad, but because it had a hidden assumption that would keep causing incidents as we scaled.

The test I use: would this debt meaningfully slow down or break the next important thing we’re trying to do? If yes, it gets scheduled, sized, and shipped like any feature. No exceptions.

Shrink — reduce the blast radius without a full rewrite.

Some debt is too risky to leave completely untouched but too expensive to eliminate entirely right now. The goal isn’t to fix it — it’s to make sure that when it eventually breaks, the damage is contained and diagnosable.

In practice this usually means: adding observability (logging, alerting, tracing), writing a runbook so anyone on the team can respond to an incident in that area, and adding a thin abstraction layer so the messy implementation can be replaced incrementally later without touching everything that depends on it.

This is the category most leads skip. They either fix things fully or leave them alone. The shrink move is underused and genuinely powerful.

Protect — leave it alone deliberately.

This one surprises people. Some debt is doing a job. It’s ugly, it’s old, it probably violates three patterns we care about today, but it is stable, well-understood by the team, and in a part of the system that isn’t actively changing.

Rewriting stable code is one of the most reliable ways to introduce new bugs. I’ve seen teams spend a quarter cleaning up a module that was working fine, only to introduce a regression that took another quarter to hunt down. The rewrite felt productive. It wasn’t.

The rule here is: if it’s stable, if it’s not in the path of active development, and if it fails loudly when it eventually breaks, protect it. Don’t touch it. Don’t let a new engineer refactor it as a “good first task.” Write down what it does, why it exists, and where the bodies are buried — then leave it alone.

Watch — not urgent, but not invisible either.

This is debt that doesn’t meet the bar for Kill or Shrink right now, but shouldn’t just disappear into the backlog either. It goes on a watchlist with a specific trigger: “we revisit this when X happens.” X might be “when we add a second service that depends on this,” or “when we hire a third engineer who needs to understand this codebase,” or “when we hit 10x current traffic.”

The watchlist prevents the debt from becoming invisible. Invisible debt is how you end up with 47 items in a backlog that nobody actually understands anymore.

How I Have This Conversation With Product

The hardest part of managing tech debt as a lead isn’t the technical judgment. It’s the conversation with your product manager or your director when they want to know why sprint capacity is going toward something users can’t see.

I’ve tried many framings over the years. The one that works is anchoring debt to a specific future capability the business cares about.

“This piece of the system is the bottleneck that will slow down feature X by two sprints if we don’t address it before we start building.”

“This module has a failure mode that, based on our growth trajectory, will cause an outage in production within the next six months. Here’s why.”

Notice what I’m not saying: “this code is messy” or “we really should clean this up.” Those are engineering aesthetics. Product doesn’t have to care about engineering aesthetics, and honestly they shouldn’t have to.

What product does care about is delivery velocity, reliability, and whether you’re going to be able to build what they need to build next. Frame the debt in those terms and the conversation stops being a negotiation between “technical purity” and “shipping features” — because it was never really about that in the first place.

The Debt I Learned to Protect

There’s one category of debt that I used to kill on sight that I now actively protect: intentional shortcuts taken under real constraints.

When a team makes a deliberate, documented decision to cut a corner because shipping mattered more than perfection in that moment — that’s not the same as accidental mess. It’s a calculated bet. And some of those bets age fine.

The Vue components in our admin panel were written fast, inconsistent with the design system, and held together with more watchers than I’d like. But they’ve been stable for two years, they’re used by maybe fifteen internal users, and nobody is touching them. We have a document that says: “this module is intentionally rough; it was built in a two-week sprint to unblock ops; it is not the pattern we follow elsewhere.”

That documentation makes it protected, not embarrassing. It means a new lead engineer joining the team doesn’t spend their first week trying to “fix” something that doesn’t need fixing. Context is the difference between debt that’s dangerous and debt that’s just old.

The Habit That Changed Everything

The single practice that made my debt decisions more defensible than anything else: writing a short tech debt log entry every time I make a deliberate technical shortcut.

Not a ticket. Not a comment in the code. A short entry in a shared doc that says: what we built, what we knowingly left rough, why we made that call, and what the trigger condition is for when it should be revisited.

Three sentences. Takes two minutes. Means that six months later, when someone asks “why is this done this way,” there’s an answer — and the answer tells you whether it’s debt worth killing or debt worth protecting.

Most tech debt isn’t the result of bad engineers making bad decisions. It’s the result of good engineers making reasonable decisions under constraints, and then never writing down the context. The code survives. The reasoning doesn’t. And without the reasoning, everything looks like a mess that needs cleaning up.

The Framework in One Paragraph

When a piece of tech debt comes to my attention, I ask: is it blocking something active, or sitting dormant? Does it fail catastrophically and silently, or loudly and cleanly? Based on that, it goes into one of four buckets — Kill, Shrink, Protect, or Watch. I frame the Kill and Shrink decisions for product in terms of delivery impact, not code quality. And I document every intentional shortcut we take so future me — and future team members — have the context to make the same judgment calls without starting from scratch.

That’s it. No scoring matrix. No spreadsheet. Just two questions, four buckets, and the discipline to write things down.

One Last Thing

The lead engineers I’ve seen struggle most with tech debt aren’t the ones who make bad technical judgments. They’re the ones who treat it as a binary — clean vs messy, fix vs ignore.

Real systems aren’t clean. They’re living things with history, context, and accumulated decisions made by people who were doing their best at the time. Your job isn’t to eliminate all the debt. It’s to know which debt is quietly holding up the ceiling, which debt is safe to walk past, and which debt is about to become someone else’s 2am incident.

The difference between those three things is almost entirely about context — and almost entirely absent from the codebase itself.

Technical debt becomes a business problem when it slows delivery, increases operational risk, or makes systems harder to evolve. Simform helps organizations modernize applications, improve software maintainability, and make pragmatic engineering decisions that balance long-term architecture with near-term business priorities.

For more updates on the latest tools and technologies, follow the Simform Engineering blog.

Follow us: Twitter | LinkedIn


메타데이터
post_id
a2c9bf88d36c
slug
how-i-decide-which-tech-debt-to-kill-and-which-to-protect-a2c9bf88d36c
url
https://medium.com/simform-engineering/how-i-decide-which-tech-debt-to-kill-and-which-to-protect-a2c9bf88d36c
canonical_url
https://medium.com/simform-engineering/how-i-decide-which-tech-debt-to-kill-and-which-to-protect-a2c9bf88d36c
author_url
https://medium.com/@milan.dadhaniya
status
ok
fetched_at
2026-07-09 16:18:44