Tech Debt Isn't a Monolith
When someone says "we have a lot of tech debt," they could mean a dozen different things. A deprecated library nobody's updated. A module with zero tests that everyone's afraid to touch. An architecture decision from 2019 that made sense then but doesn't fit current scale. A folder of PHP scripts powering a critical billing process.
Lumping all of these together as "tech debt" and arguing for a "tech debt sprint" doesn't work. Each type requires different treatment, different urgency, and different investment.
Classifying Your Debt
I've found this four-category framework useful for prioritization discussions:
Reckless debt — Shortcuts taken knowingly under time pressure. "We'll skip tests for the launch." "Just hardcode the config for now." This is the debt most people think of, and it's usually the smallest category.
Outdated debt — The codebase didn't age well. Libraries have better alternatives now. Patterns that were standard five years ago are anti-patterns today. The React class components that worked fine in 2019 but the team now writes everything in hooks. This debt accumulates passively.
Bit rot — Working code that's degraded through accumulated small changes. Each individual change was reasonable, but the cumulative effect is a mess. A module that started as 200 clean lines and grew to 2,000 lines of spaghetti through fifty PRs over three years.
Architecture debt — The system's structure doesn't match current requirements. The monolith that needs to be split. The synchronous pipeline that needs to be async. The database schema designed for 10,000 users struggling at a million. This is the most expensive to fix and the most expensive to ignore.
Prioritization Framework
Not all tech debt needs to be paid off. Some debt is cheap to carry. The question isn't "how much debt do we have?" but "which debt is actively slowing us down?"
Rate each debt item on two axes:
- Interest rate — How much does this debt cost per unit of time? A flaky test suite that wastes 2 developer-hours per week has a high interest rate. A deprecated library with no known vulnerabilities has a low interest rate.
- Risk — What's the worst case if this debt causes a failure? A billing system built on assumptions that no longer hold is high risk. An ugly admin UI is low risk.
Prioritize: high interest + high risk first. Low interest + low risk can wait indefinitely or be addressed opportunistically.
The 20% Myth
Many teams adopt a "20% of sprint capacity for tech debt" rule. This sounds reasonable but usually fails because:
- Feature work expands to fill available time, pushing tech debt tasks to the next sprint
- 20% isn't enough for large debt items but is too much if you don't have a backlog of small, well-scoped debt tasks
- It frames debt work as separate from feature work, when they should be interleaved
What works better:
Boy Scout Rule — Leave every file you touch slightly better than you found it. Rename a confusing variable. Extract a helper function. Add a missing type annotation. This handles bit rot continuously without dedicated time.
Embed in feature work — When building a feature requires touching a legacy module, scope the modernization of that module into the feature work. "Adding search to the products page" includes "refactoring the products module to support the search feature." Product managers don't need to approve a separate "refactoring" ticket.
Dedicated debt sprints quarterly — For larger architectural work (database migration, framework upgrade, service extraction), block off a focused week or two where the team addresses the biggest debt items. This works because it's time-boxed and has visible outcomes, unlike the slow drain of the 20% approach.
Tracking and Communicating Debt
Technical debt needs to be visible to non-technical stakeholders. They won't understand "the authentication module uses an outdated pattern," but they'll understand "every security feature takes 3x longer to build because of how the auth system is structured."
Track debt items in your regular issue tracker (not a separate system nobody checks). Tag them consistently. For each item, include:
- What the debt is (specific, not vague)
- What it costs (in developer time, incident risk, or blocked features)
- What fixing it would look like (scope and estimated effort)
- When it was last evaluated (debts change priority as the codebase evolves)
Review the debt backlog quarterly. Remove items that are no longer relevant (the code was deleted, the risk was mitigated another way). Reprioritize based on current pain points.
When to Accept the Debt
Sometimes the right answer is to live with it. If a legacy system works, has no security issues, and isn't actively slowing down development, rewriting it because it's "ugly" is a waste of resources. I've seen teams spend six months rewriting a perfectly functional service, only to introduce new bugs and deliver zero user value.
Pay down debt that's actively costing you. Accept debt that's stable and contained. The distinction matters.