Technical Debt Is Not a Bug: A Strategic Guide to Managing It
TL;DR
Technical debt is a tool, not a disease. Take it on intentionally when speed matters (launches, experiments, time-sensitive features) and pay it down before it compounds. Track it like financial debt — know what you owe, the interest rate (velocity cost), and the payment schedule. The 20% rule works: dedicate 20% of sprint capacity to debt reduction, every sprint, non-negotiable. Refactor the code you change most (git log tells you where). And stop calling everything you don't like 'tech debt' — some code is just old, and that's fine.
I once joined a project where deploying a one-line CSS change took three days. Not because of process or approvals — because the build was so fragile that any change had a 40% chance of breaking something unrelated. The team had been "moving fast" for two years without paying down any debt, and now they couldn't move at all.
That's the thing about technical debt: the interest payments are invisible until they're not. One day you're shipping features weekly, and then gradually — so gradually nobody notices — it becomes biweekly, then monthly, then "we're doing a Big Refactor™" which is really just admitting you've been ignoring the credit card bill for too long.
I've been on both sides of this. I've taken on debt strategically and shipped products that wouldn't have existed otherwise. I've also inherited codebases where the debt was so crushing that the team spent 80% of their time fighting the code and 20% actually building features. Here's what I've learned about walking that line.
Technical Debt Is a Tool
Let me be clear: technical debt is not inherently bad. It's a tool. Like financial debt, it's useful when taken on intentionally with a repayment plan.
┌─────────────────────────────────────────────────────────────────┐
│ The Technical Debt Quadrant │
├─────────────────────────┬───────────────────────────────────────┤
│ │ Deliberate │ Inadvertent │
├─────────────────────────┼──────────────────────┼────────────────┤
│ Reckless │ "We don't have time │ "What's a │
│ │ for design" │ design pattern?"│
│ │ ↑ Dangerous │ ↑ Needs │
│ │ │ mentoring │
├─────────────────────────┼──────────────────────┼────────────────┤
│ Prudent │ "Ship now, refactor │ "Now we know │
│ │ next sprint" │ how we should │
│ │ ↑ Strategic! │ have done it" │
│ │ │ ↑ Natural │
└─────────────────────────┴──────────────────────┴────────────────┘
The top-left quadrant is what kills projects. The bottom-left is what makes great engineers great — knowing when to take a shortcut and having the discipline to circle back.
Debt vs. Rust
Not everything old is debt. Code that works, is tested, and doesn't slow you down isn't debt — it's just mature. Don't refactor working code because it uses an older pattern. Refactor code that's actively costing you time, bugs, or velocity. There's a big difference between "this code is ugly" and "this code is expensive."
When to Take On Debt (Intentionally)
Launch deadlines. You have a conference demo in two weeks. Hardcode the feature flag, skip the abstraction layer, ship it. Just write the TODO and create the ticket.
Experiments. Building a feature to test a hypothesis? Don't architect it for scale. Build the simplest version that proves or disproves the idea. If it works, you'll rebuild it properly with the knowledge you gained.
First customers. Early-stage products need to ship. Period. Your first ten customers don't care about your database migration strategy. They care about whether the product solves their problem.
Time-sensitive opportunities. A partnership requires an integration by Friday. Build it quick, ship it, and refactor it into a proper abstraction when you're not under the gun.
The common thread: in all these cases, you're making a conscious decision to trade quality for speed, with a specific plan to pay it back. That's strategic debt. The moment you stop tracking it or planning repayment, it becomes reckless debt.
How to Track Technical Debt
You can't manage what you don't measure. Here's the system I use:
// I literally put debt in the code as structured comments
// These get picked up by a script that generates a debt report
// DEBT: Hardcoded tenant ID for single-tenant MVP
// COST: Blocks multi-tenant launch (est. 3 days to fix)
// TAKEN: 2026-01-15 (launch deadline)
// OWNER: @osvaldo
// TICKET: JIRA-1234
const TENANT_ID = "tenant_acme_001";
// DEBT: N+1 query in dashboard loader
// COST: ~200ms extra latency per dashboard load
// TAKEN: 2026-02-01 (shipped fast for demo)
// OWNER: @osvaldo
// TICKET: JIRA-1456
const users = await Promise.all(
teamIds.map(id => db.users.findByTeam(id))
);┌─────────────────────────────────────────────────────────────────┐
│ Debt Tracking Dashboard (simplified) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Total Debt Items: 23 │
│ ├── Critical (blocks features): 4 │
│ ├── High (slows velocity): 8 │
│ ├── Medium (annoyance): 7 │
│ └── Low (cleanup): 4 │
│ │
│ Estimated Paydown: │
│ ├── Critical: 12 dev-days │
│ ├── High: 18 dev-days │
│ ├── Total: ~45 dev-days (9 sprints at 20%) │
│ │
│ Oldest Debt: 4 months (auth module - MIGRATE NOW) │
│ Newest Debt: 3 days (dashboard N+1 - scheduled next sprint) │
│ │
│ Velocity Impact: ~30% slower than 6 months ago │
│ Bug Rate from Debt: 2.3 bugs/sprint attributed to debt areas │
│ │
└─────────────────────────────────────────────────────────────────┘
The 20% Rule
The single most effective strategy I've found: dedicate 20% of every sprint to debt reduction. Not "when we have time." Not "next quarter." Every sprint. Non-negotiable.
Here's why it works:
- It's small enough to not feel threatening. Managers rarely push back on 20%. They will push back on "we need a month to refactor."
- It compounds. 20% per sprint adds up to serious progress over a quarter. You'll be amazed at how much cleaner the codebase gets.
- It prevents accumulation. Regular small payments prevent debt from reaching the point where you need a Big Rewrite.
- It keeps the team motivated. Engineers hate working in messy code. Knowing they get to improve it regularly reduces burnout.
┌─────────────────────────────────────────────────────────────────┐
│ Where to Focus Debt Repayment │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Run this command │
│ │
│ git log --since="3 months" --name-only --format="" | │
│ sort | uniq -c | sort -rn | head -20 │
│ │
│ Step 2: The files that appear most often are your hot spots │
│ │
│ Step 3: Refactor THOSE files first │
│ │
│ Why: Files that change often are files that matter. │
│ Cleaning up a file nobody touches is busywork. │
│ Cleaning up a file that's modified 5x per sprint? │
│ That's an investment with immediate returns. │
│ │
└─────────────────────────────────────────────────────────────────┘
The Boy Scout Rule + Git Log
"Leave the code cleaner than you found it" is good advice, but unfocused. Combine it with git log analysis: leave the most-changed code cleaner than you found it. Your git history tells you exactly where the pain is — files with high churn and high bug rates are where debt repayment has the biggest ROI.
Refactor, Don't Rewrite
I have a rule: never propose a rewrite. Propose a refactoring plan with milestones.
Rewrites fail for predictable reasons. The team underestimates the scope (always). The old system keeps getting features that the rewrite has to catch up with (always). The rewrite introduces new bugs because you're reimplementing business logic from memory instead of from tests (always). And six months in, you have two broken systems instead of one working one.
The strangler fig pattern is your friend: wrap the old system, replace it piece by piece, and never have a "big bang" migration day.
// Strangler fig: new code wraps old code
// Phase 1: Route through new system, fall back to old
async function getUser(id: string): Promise<User> {
try {
// Try the new service first
return await newUserService.getById(id);
} catch {
// Fall back to legacy
return await legacyUserService.getById(id);
}
}
// Phase 2: Once confident, remove the fallback
async function getUser(id: string): Promise<User> {
return await newUserService.getById(id);
}
// Phase 3: Delete legacyUserService entirelyCommunicating Debt to Non-Technical Stakeholders
The biggest mistake engineers make: describing debt in technical terms to non-technical people. "We need to refactor the authentication module to use OAuth2 instead of our custom JWT implementation" means absolutely nothing to your product manager.
Instead:
- Before: "We need to refactor the auth module" (engineer-speak)
- After: "Our login system causes 2 security tickets per month and makes adding new features take 3x longer. A 2-week fix eliminates those tickets and cuts feature delivery time by 40%." (business-speak)
Frame every debt conversation around three things stakeholders care about:
- Velocity: "This debt is making us X% slower"
- Reliability: "This debt causes Y bugs per month"
- Risk: "This debt exposes us to Z"
If you can't articulate the business impact, maybe the debt isn't actually that important. And that's fine — not all debt needs to be paid immediately. Some debt is cheap to carry. The expensive debt is what you focus on.
The Debt Audit
Every quarter, I do a debt audit. It takes about 2 hours and saves weeks of misdirected effort:
- Inventory: What debt do we have? (check code comments, JIRA, team knowledge)
- Cost: What is each item costing us? (velocity, bugs, risk)
- Priority: Rank by cost-to-fix vs. cost-to-carry ratio
- Plan: Schedule the top 5 items into the next quarter's sprints
- Retire: Remove items that are no longer relevant (code was deleted, feature was killed)
The audit is also a great time to celebrate what you've paid down. The team needs to see that their 20% investment is making a difference. Show the before/after: build times, bug rates, deployment frequency. Make the progress visible.
The Bottom Line
Technical debt is inevitable. The question isn't whether you'll have it — it's whether you'll manage it or let it manage you. The teams that thrive are the ones that treat debt like a financial tool: take it on strategically, track it rigorously, pay it down consistently, and never let it compound past the point of no return.
And if you're reading this while staring at a codebase that's already past that point: it's not too late. Start with the 20% rule. Start with the hottest file in your git log. Start with one thing that makes your daily work painful. The compound effect of consistent, small improvements is more powerful than any Big Rewrite will ever be.
The best time to start paying down debt was six months ago. The second best time is this sprint.
Frequently Asked Questions
Related Articles
Clean Code Principles That Actually Matter
A practical guide to writing maintainable code, focusing on the principles that have the highest impact in real-world software projects, with examples in multiple languages.
Designing CI/CD Pipelines That Don't Make You Want to Quit
Battle-tested CI/CD pipeline design with GitHub Actions — fast feedback loops, parallel testing, caching strategies, preview deployments, flaky test quarantine, and the 'green main' philosophy. Written by someone who's been paged at 3 AM because the pipeline was lying.
Don't miss a post
Articles on AI, engineering, and lessons I learn building things. No spam, I promise.
Osvaldo Restrepo
Senior Full Stack AI & Software Engineer. Building production AI systems that solve real problems.