Skip to main content

Technical Debt: When to Pay It Down

Ryan Dahlberg
Ryan Dahlberg
December 1, 2025 13 min read
Share:
Technical Debt: When to Pay It Down

Technical Debt: When to Pay It Down

Your team ships a feature quickly with “temporary” code. Six months later, that temporary solution is still there, slowing down every new feature. The team wants to refactor. Management wants new features. Both sides are frustrated.

Welcome to the technical debt dilemma.

After years of building production systems—from AI orchestration platforms like Cortex to infrastructure automation at scale—I’ve learned that managing technical debt isn’t about eliminating it. It’s about making strategic decisions about when to pay it down and when to let it ride.

Let me share a framework that actually works.

What Is Technical Debt?

Ward Cunningham coined the term “technical debt” in 1992 as a metaphor for choosing quick-and-dirty solutions over better approaches that take longer.

Like financial debt, technical debt has:

  • Principal: The extra work created by choosing the easy solution
  • Interest: The ongoing cost of working with suboptimal code
  • Compounding: The interest gets worse over time

Types of Technical Debt

Not all technical debt is created equal.

1. Deliberate and Prudent ✅

“We know the right solution, but shipping fast is more important right now.”

// TODO: Replace this in-memory cache with Redis before scaling to multiple servers
const cache = new Map<string, User>();

function getUser(id: string): User | undefined {
  return cache.get(id);
}

Characteristics:

  • Conscious decision
  • Clear path to resolution
  • Documented reasoning
  • Time-boxed

This is good debt. Like a mortgage, you’re deliberately taking it on to achieve something valuable.

2. Deliberate and Reckless ❌

“We don’t have time to do it right.”

// No validation, no error handling, no tests
function processPayment(card: any): any {
  return stripe.charge(card.number, card.amount);
}

Characteristics:

  • Knowingly shipping bad code
  • No plan to fix it
  • Usually under pressure

This is bad debt. Like payday loans, the interest rate is crushing.

3. Accidental and Prudent 😐

“Now we know how we should have done it.”

You made the best decision with the information you had. Later, you learned better.

// Six months ago: Seemed reasonable
class UserManager {
  handleEverything(user: User) {
    // 500 lines of mixed responsibilities
  }
}

// Today: We understand the domain better
// Should be: UserAuthService, UserProfileService, UserPreferencesService

Characteristics:

  • Discovered through experience
  • Learning opportunity
  • Often inevitable

This is learning debt. The cost of gaining knowledge.

4. Accidental and Reckless 💀

“What is layered architecture?”

Poor fundamentals, lack of knowledge, no code review.

// Direct database queries in React components
function UserProfile() {
  const user = await db.query('SELECT * FROM users WHERE id = ' + userId);
  return <div>{user.name}</div>;
}

Characteristics:

  • Result of skill gaps
  • Systemic problems
  • Requires education

This is ignorance debt. The most expensive kind.

The True Cost of Technical Debt

1. Slower Development Velocity

Every new feature takes longer:

// Want to add a new payment method?
// First you have to understand this mess:

function processPayment(data: any) {
  if (data.type === 'card') {
    // 50 lines of card logic
  } else if (data.type === 'paypal') {
    // 40 lines of PayPal logic
  } else if (data.type === 'crypto') {
    // 60 lines of crypto logic
  }
  // ... 200 more lines
}

// Instead of:

interface PaymentProcessor {
  process(amount: number): Promise<Result>;
}

// Just add a new class implementing the interface

Cost: What used to take 1 day now takes 3 days.

2. Increased Bug Rate

Tangled code breeds bugs:

// Changing one thing breaks three other things
let userState: any = {};

function updateUser(field: string, value: any) {
  userState[field] = value;
  recalculateTotal(); // Why does this need to happen?
  refreshCache();     // What cache?
  notifyListeners();  // What listeners?
}

Cost: More time debugging, more customer-facing bugs, more urgent hotfixes.

3. Developer Morale

Nobody wants to work in a codebase that fights them:

“Every change is terrifying. I never know what I’ll break.”

Cost: Burnout, turnover, difficulty hiring.

4. Opportunity Cost

Time spent fighting bad code is time not spent building features:

// Reality with high debt
Week 1: Plan feature
Week 2: Fight with existing code
Week 3: Finally implement feature
Week 4: Fix bugs introduced by messy code

// Reality with low debt
Week 1: Implement feature, ship it
Week 2-4: Build three more features

Cost: Competitors move faster, customers wait longer.

When to Pay Down Debt

Not all debt deserves immediate attention. Use this framework:

The Debt Priority Matrix

ImpactFrequencyPriority
HighHighPay now
HighLowPay soon
LowHighPay eventually
LowLowLeave it

High Impact + High Frequency = Pay Now 🔥

// This authentication bug affects every user, every request
function authenticate(token: string): boolean {
  // SECURITY: This has false positives!
  return token.length > 10; // Completely wrong
}

Drop everything and fix this.

Characteristics:

  • Affects critical paths
  • Touched constantly
  • Blocks other work
  • High risk

High Impact + Low Frequency = Pay Soon 📅

// This annual report generator is a mess, but only runs once a year
function generateAnnualReport() {
  // 1000 lines of spaghetti
  // Takes a full day to modify
  // But only matters in December
}

Schedule dedicated time to fix this.

Characteristics:

  • Important but not urgent
  • Painful when you hit it
  • Predictable schedule
  • Good refactoring candidate

Low Impact + High Frequency = Pay Eventually ⏰

// This logging utility is clunky but doesn't block work
function log(msg: string) {
  console.log('[' + new Date().toISOString() + '] ' + msg);
}

// Would be nicer as a proper logger, but not critical

Improve incrementally over time.

Characteristics:

  • Minor annoyance
  • Doesn’t slow down features
  • Easy to work around
  • Nice-to-have improvement

Low Impact + Low Frequency = Leave It 🤷

// This migration script from 2019 still works
function migrateOldDataFormat() {
  // Not pretty, but runs once per year
  // for a legacy integration nobody uses
}

Don’t waste time on this.

Characteristics:

  • Rarely touched
  • Minimal impact
  • Working as-is
  • Not worth the effort

The 10x Rule

Pay down debt when the future cost of keeping it exceeds 10x the cost of fixing it.

Why 10x? Because refactoring has hidden costs:

  • Risk of introducing bugs
  • Time spent testing
  • Team coordination
  • Opportunity cost

Example Calculation:

Current pain: 30 minutes of frustration per feature
Features per quarter: 10
Annual cost: 30 min × 10 features × 4 quarters = 20 hours

Refactoring cost: 16 hours
Refactoring risk: 4 hours of bug fixes

Total refactoring cost: 20 hours
Annual savings: 20 hours
Break-even: 1 year

10x threshold: If you'll work on this code for 10+ years, refactor now.

If you’re touching the code monthly? Fix it immediately.

How to Pay Down Debt

The Strangler Fig Pattern

Don’t rewrite. Gradually replace.

// Old system (messy)
function processPaymentOld(data: any): any {
  // 500 lines of spaghetti
}

// New system (clean)
interface PaymentProcessor {
  process(payment: Payment): Promise<Result>;
}

// Adapter: Route traffic gradually
function processPayment(data: any): any {
  if (shouldUseNewSystem(data)) {
    return newPaymentSystem.process(data);
  } else {
    return processPaymentOld(data);
  }
}

// Gradually increase percentage using new system
function shouldUseNewSystem(data: any): boolean {
  const userId = data.userId;
  return hashCode(userId) % 100 < ROLLOUT_PERCENTAGE;
}

Benefits:

  • Low risk (can roll back)
  • Continuous validation
  • No big-bang cutover
  • Features can continue shipping

The Boy Scout Rule

“Leave the code better than you found it.”

// You're adding a feature here:
function getUserData(id: string) {
  const u = db.query('SELECT * FROM users WHERE id = ' + id);
  return u;
}

// While you're here, make small improvements:
function getUserData(id: string): User {
  // Fixed: SQL injection vulnerability
  // Fixed: Added return type
  // Fixed: Used parameterized query
  return db.query('SELECT * FROM users WHERE id = $1', [id]);
}

Benefits:

  • Debt decreases gradually
  • No dedicated refactoring time needed
  • Low risk (small changes)
  • Builds culture of quality

The Dedicated Sprint

For large refactorings, allocate focused time:

Sprint planning:
- 70% features
- 30% tech debt

One sprint per quarter:
- 20% features
- 80% tech debt (the "quality sprint")

Benefits:

  • Uninterrupted focus
  • Can tackle big problems
  • Predictable schedule
  • Management buy-in

Communicating About Technical Debt

The hardest part isn’t the refactoring—it’s getting permission to do it.

Speak the Business Language

Don’t say:

“We need to refactor the user service because the coupling is too tight and it violates SOLID principles.”

Say:

“Our user features take 3 days instead of 1 day because of how the code is structured. If we spend 2 weeks cleaning it up, we’ll ship user features 2x faster for the next year. That’s 26 days saved per year.”

Frame it as ROI, not technical purity.

The Feature Velocity Graph

Show the trend:

Story Points per Sprint

Q1: 50 points
Q2: 45 points (-10%)
Q3: 38 points (-24%)
Q4: 30 points (-40%)

With refactoring:
Q1-Q2: 40 points (investment)
Q3: 55 points
Q4: 60 points (+20% from baseline)

Visual proof that debt is slowing you down.

The Risk Register

Make debt visible:

AreaRiskImpactLikelihoodPriority
Auth systemSecurity breachCriticalMediumHigh
Payment flowLost revenueHighHighHigh
Report generatorSlow reportsLowLowLow

Executives understand risk management.

Preventing Technical Debt

Design Reviews

Catch debt before it’s written:

Before implementing:
1. Write a 1-page design doc
2. Review with 2 senior engineers
3. Identify shortcuts and document them
4. Approve OR iterate

Cost: 2 hours upfront
Savings: 20 hours of refactoring later

Definition of Done

Make quality non-negotiable:

Feature is "done" when:
✅ Functionality complete
✅ Tests passing (>80% coverage)
✅ Code reviewed by 2 engineers
✅ Documentation updated
✅ No known security vulnerabilities
✅ Performance benchmarked

If it doesn’t meet the bar, it’s not done.

Debt Budget

Allocate capacity:

Sprint capacity: 100 points

Features: 70 points
Tech debt: 20 points
Bugs: 10 points

Make debt paydown a regular part of the schedule.

Automated Quality Gates

Prevent debt from merging:

# GitHub Actions example
quality-gate:
  - name: Code Coverage
    threshold: 80%
  - name: Complexity
    max: 10
  - name: Security Scan
    block_on: critical
  - name: Performance
    max_response_time: 200ms

Automation prevents “I’ll fix it later” from shipping.

Real-World Examples

Example 1: The 10-Minute Test Suite

Problem: Tests took 10 minutes to run. Developers stopped running them.

Debt cost:

  • 50 test runs per day × 10 minutes = 500 minutes of waiting
  • Bugs reaching production (tests not run)
  • Developer frustration

Fix: 1 week to parallelize tests and optimize slow ones.

Result: Tests now run in 90 seconds. Team runs tests constantly. Bugs caught early.

ROI: 1 week investment saved 400+ minutes per day.

Example 2: The God Object

Problem: UserService did everything. 2000 lines, 15 responsibilities.

Debt cost:

  • Every user feature took 3 days (hard to understand)
  • High bug rate (changing one thing broke others)
  • Onboarding took 2 weeks (new devs overwhelmed)

Fix: 2 sprints to split into focused services.

Result: 8 services with clear boundaries. User features take 1 day. Bugs down 60%.

ROI: 4 weeks investment paid back in 8 weeks.

Example 3: The Database Migration

Problem: Still using a database schema from 2015. Inefficient, hard to extend.

Debt cost:

  • Queries getting slower
  • New features require complex workarounds
  • Risk of scaling issues

Fix: Too risky to fix immediately.

Strategy:

  1. Add database abstraction layer (2 weeks)
  2. Write to both old and new schema (2 weeks)
  3. Migrate data in background (4 weeks)
  4. Read from new schema (1 week)
  5. Remove old schema (1 week)

Result: Zero downtime migration over 10 weeks.

When NOT to Pay Down Debt

You’re Exploring

Building a prototype or MVP? Embrace the debt.

// MVP: Quick and dirty is fine
function processOrder(order: any): any {
  stripe.charge(order.total, order.card);
  return { success: true };
}

// After product-market fit: Then refactor

Don’t optimize code that might be thrown away.

The Code Is Dying

If you’re migrating off a system, don’t improve it:

// Legacy system being replaced
function oldPaymentFlow() {
  // Don't refactor this
  // It'll be gone in 6 months
}

Let dead code die.

Low-Risk, Low-Impact

Some debt just doesn’t matter:

// One-off migration script
function migrate2023Data() {
  // Not pretty, but runs once
}

Perfect is the enemy of done.

Action Plan

This Week

  1. Identify your top 3 pain points

    • What code do you dread touching?
    • What causes the most bugs?
    • What slows down features?
  2. Quantify the cost

    • How much time does each pain point waste?
    • How often do you hit it?
    • What’s the business impact?
  3. Prioritize using the matrix

    • High impact + high frequency = Pay now
    • Low impact + low frequency = Leave it

This Month

  1. Fix one high-priority debt item

    • Use the strangler fig pattern
    • Make it a team priority
    • Measure the improvement
  2. Implement the boy scout rule

    • Every PR leaves code better
    • Small improvements add up
    • Build the habit
  3. Create a debt register

    • List known debt items
    • Track impact and cost
    • Review monthly

This Quarter

  1. Schedule a quality sprint

    • Dedicate 2 weeks to debt
    • Tackle the second-tier items
    • Measure velocity improvement
  2. Add quality gates

    • Automated tests
    • Code coverage
    • Performance benchmarks
    • Security scans
  3. Establish a debt budget

    • 20-30% capacity for quality
    • Regular debt paydown
    • Prevent debt accumulation

Conclusion: Technical Debt Is a Tool

Technical debt isn’t inherently bad. Like financial debt, it’s a tool for managing trade-offs.

Good use of debt:

  • Ship MVP to validate idea
  • Beat competitor to market
  • Test hypothesis cheaply

Bad use of debt:

  • Avoid learning proper techniques
  • Ship recklessly under pressure
  • Ignore compounding interest

The key is intentionality:

  1. Know when you’re taking on debt
  2. Understand the cost
  3. Have a plan to pay it down
  4. Execute that plan

The best teams don’t have zero technical debt. They have managed technical debt—conscious decisions that balance speed and quality.

Debt becomes a crisis when you pretend it doesn’t exist.

Track it. Measure it. Pay it down strategically.

Your future self (and your team) will thank you.

Resources


Part of the Developer Skills series. Building software that scales sustainably.

How does your team handle technical debt? What strategies have worked (or failed) for you? I’d love to hear your war stories and victories.

#Code Quality #Technical Debt #Software Engineering #Refactoring #Engineering Leadership