Technical Debt: How to Recognize It and Pay It Down Gradually
We had a deadline. I hardcoded tax rates for five states directly in the checkout function. "We'll clean it up after launch," I told myself.
That was two years ago. The function now has 47 states hardcoded. Adding a new state takes 30 minutes of careful scrolling. Every tax law change is a merge conflict nightmare. I pay interest on that "quick fix" every single week.
That's technical debt—the cost of choosing fast over right. It's not always bad. Sometimes you need the loan. But if you only pay the interest, you eventually go bankrupt.
Not All Debt Is Equal
There are different kinds:
- Reckless & Deliberate: "We don't have time for tests, ship it." Dangerous.
- Reckless & Inadvertent: "I didn't know there was a better way." Training problem.
- Prudent & Deliberate: "We'll hardcode this for the trade show and fix it next sprint." Strategic.
- Prudent & Inadvertent: "Now that we've built it, we see how we should have designed it." Learning.
My tax rates? Prudent and deliberate—at first. But I never scheduled the payback. It became reckless.
How to Spot High-Interest Debt
Some debt barely costs you anything. Some drains hours every week. Look for these symptoms:
- Rigidity: You want to change a button color but have to modify database schemas. Everything is connected.
- Fragility: You fix a bug in Users, and Billing breaks. Changes cascade unpredictably.
- Immobility: You want to reuse the validation logic, but it's tangled with the UI. Can't extract it.
Paying It Down: The Boy Scout Rule
"Leave the campground cleaner than you found it."
Don't stop everything for a "refactoring sprint"—management rarely approves them, and they deliver no user value.
Instead, when you touch a file to add a feature, spend 10 extra minutes cleaning up. Rename a confusing variable. Extract a long function. Add one missing test.
// Before: Adding Texas support
if (state === 'NY') return price * 1.08;
if (state === 'CA') return price * 1.07;
if (state === 'TX') return price * 1.06; // New line
// ... 44 more states
// After: Same feature, paying down debt
const TAX_RATES = {
'NY': 0.08,
'CA': 0.07,
'TX': 0.06 // Easy to add
};
function getTaxRate(state) {
return TAX_RATES[state] || 0;
}
Same feature delivered. But now adding the next state takes 10 seconds instead of 30 minutes.
The 20% Buffer
When estimating tasks, include cleanup time. If a feature takes 4 days "quick and dirty," estimate 5. Use the 5th day to write tests, clean up hacks, document decisions.
This prevents new debt from accumulating faster than you pay it down.
Make It Visible
Debt is invisible to non-technical stakeholders. Make it concrete:
- Search for
// TODOcomments. Is the count rising? - Track test coverage. Dropping coverage means accumulating debt.
- Use git analytics to find "hotspots"—files changed frequently with high complexity. Those are your expensive loans.
Zero Debt Is Wrong Too
Ironically, zero technical debt usually means you over-engineered or shipped too late. Some debt is strategic. The goal isn't elimination—it's management.
Keep the interest payments low enough that you can ship features with confidence. That's sustainable engineering.