Skip to main content
Engineering

Technical Debt You Should Actually Pay Down (and What to Leave)

Ravinder··8 min read
EngineeringTechnical DebtArchitectureTeam HealthEngineering Leadership
Share:
Technical Debt You Should Actually Pay Down (and What to Leave)

The Lie Engineers Tell Themselves

"We will clean it up later."

Later never comes, or it comes at the worst possible time — when you are three sprints into a deadline and someone decides that now is the time to refactor the authentication module. Technical debt is the background radiation of software engineering: always present, always accumulating, occasionally lethal, and wildly misunderstood.

The most destructive belief about technical debt is that all of it should eventually be paid. It should not. Some debt costs more to fix than to carry. Some debt lives in code nobody will touch again. Some debt is not really debt — it is just code that irritates engineers but has no measurable impact on anything that matters.

This post is a framework for making deliberate decisions about what to pay and what to leave.


What Technical Debt Actually Is

Ward Cunningham's original metaphor was specific: technical debt is the extra work you do in the future because you chose a quick solution over a correct one today. The debt metaphor implies an interest rate — the longer you carry it, the more it costs.

But not all debt accrues interest at the same rate. A poorly named variable in a stable, never-touched utility module has near-zero interest. A tightly coupled domain object in a module that every team touches has compounding daily interest.

graph TD Debt["Technical Debt Types"] Debt --> Deliberate["Deliberate\n('We know it's wrong,\nbut we need to ship')"] Debt --> Accidental["Accidental\n('We didn't know better\nat the time')"] Debt --> Bit["Bitrot\n('The codebase evolved\naround it')"] Debt --> Environmental["Environmental\n('Dependencies moved,\nwe didn't')"] Deliberate -->|"High urgency"| PaySoon["Pay within 1-2 sprints"] Accidental -->|"Moderate"| Assess["Assess impact and schedule"] Bit -->|"Low unless blocking"| Leave["Usually leave it"] Environmental -->|"Security risk"| Security["Treat as security debt"] style PaySoon fill:#FEE2E2,stroke:#EF4444 style Security fill:#FEE2E2,stroke:#EF4444 style Assess fill:#FEF3C7,stroke:#F59E0B style Leave fill:#F3F4F6,stroke:#D1D5DB

Understanding what type of debt you have determines the right response.


The Prioritisation Framework: Two Axes

Every piece of technical debt can be placed on two axes:

  • Impact on delivery velocity: Does this debt slow down adding features or fixing bugs? Does it require deep context to change? Does it cause frequent rework?
  • Urgency: Is there an active consequence right now? Security risk, compliance gap, production instability?

This gives you four quadrants with four different responses.

quadrantChart title Technical Debt Prioritisation x-axis Low Delivery Impact --> High Delivery Impact y-axis Low Urgency --> High Urgency quadrant-1 Schedule It quadrant-2 Pay Now quadrant-3 Leave It quadrant-4 Batch It Tight coupling: [0.8, 0.3] Hardcoded secrets: [0.5, 0.95] Naming issues: [0.1, 0.2] EOL dependency: [0.4, 0.8] Missing tests: [0.7, 0.4] Outdated comments: [0.1, 0.3] Shared DB schema: [0.85, 0.35] Security CVE: [0.6, 0.98]

Quadrant 1: Pay Now (High urgency, High impact)

This is debt you address immediately. Feature work stops. The debt takes priority.

Examples:

  • Unpatched CVEs in production dependencies
  • Data integrity bugs causing silent corruption
  • Architecture that is blocking multiple teams from shipping
  • End-of-life runtime with no security support
# CVE check — run this in CI
$ mvn dependency-check:check
 
# Found: CVE-2024-XXXX in jackson-databind:2.14.0 (CRITICAL)
# Fix: upgrade to 2.15.2

The test for Pay Now debt is: if this debt remains in three months, what is the worst plausible outcome? If the answer includes data loss, breach, or regulatory action, it is Pay Now.

Quadrant 2: Schedule It (Low urgency, High impact)

This is the debt with the highest return on investment to address. It is not on fire today, but it compounds. Every sprint you do not address it, it costs you a little more in developer productivity.

Examples:

  • A tightly coupled domain module where every change requires touching six files
  • A missing abstraction layer causing duplication across teams
  • A shared database schema that prevents teams from deploying independently
  • A test suite so slow it is not running in CI

How to schedule it:

flowchart LR Identify["Identify debt\nwith cost signal\n(cycle time, rework count)"] --> Ticket["Create tech debt ticket\nwith: what, why, cost, size"] --> Sprint["Reserve 20% capacity\nevery sprint for debt"] --> Track["Track velocity\nbefore and after paydown"]

The "20% capacity" rule is the most practical approach I have seen work. You do not need a dedicated refactoring sprint. You do not need a separate debt backlog that the product team ignores. You reserve a fixed percentage of every sprint for debt that meets the scheduling criteria, and you spend it consistently.

Quadrant 3: Leave It (Low urgency, Low impact)

This is the debt that wastes engineering time to address. The change costs more in opportunity cost, merge conflicts, and review time than the benefit it delivers.

Examples:

  • Inconsistent variable naming in a stable module nobody changes
  • An over-engineered abstraction that works perfectly
  • Duplicate utility functions in separate packages with no active callers
  • A class that "should" be broken into smaller classes but works correctly and nobody touches it

The hard part of Quadrant 3 is resisting the urge. Engineers have strong aesthetic reactions to code that could be better. That aesthetic reaction is right but the action it implies — refactor everything — is wrong. The question is never "could this be better?" It is "does the improvement in this code justify the cost of changing it?"

For Quadrant 3 debt, the answer is almost always no.

Quadrant 4: Batch It (High urgency, Low impact)

This debt needs addressing but does not require urgent individual attention. Bundle it into a quarterly cleanup sprint.

Examples:

  • Dependency version bumps (no security CVEs, just keeping current)
  • Non-critical test flakiness (flaky but in non-critical paths)
  • Deprecation warnings in library upgrades
  • Logging noise or inconsistent log format
# Batch dependency updates with Renovate or Dependabot
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "maven"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      minor-patch:
        patterns: ["*"]
        update-types: ["minor", "patch"]

The Metrics That Reveal Real Debt

Teams that manage technical debt well do not rely on engineers shouting about code quality. They measure it.

graph TD Signals["Debt Signal Metrics"] Signals --> CT["Cycle time per story\n(rising → delivery debt)"] Signals --> RW["Rework rate\n(bugs from same module repeatedly)"] Signals --> MTTR["Incident MTTR\n(high → observability debt)"] Signals --> RU["Ramp-up time for new engineers\n(> 2 weeks in area → complexity debt)"] Signals --> TP["Test pass time\n(> 30min → test suite debt)"] style CT fill:#DBEAFE,stroke:#3B82F6 style RW fill:#FEE2E2,stroke:#EF4444 style MTTR fill:#FEF3C7,stroke:#F59E0B

Cycle time is the most powerful signal. If the cycle time for stories in a particular module consistently runs 2-3× longer than similar-complexity stories elsewhere, that module has delivery-impacting debt. No code review required — the data tells you.


Making the Case to Product

The conversation with product managers about technical debt fails when engineers say "the code is messy and we need to clean it up." It works when engineers say "this module adds three days to every feature that touches it, we are touching it in the next five sprints, and we estimate a one-sprint investment reduces that overhead by 80%."

The second framing is an investment thesis, not a complaint. Product managers make investment decisions every day. Give them the data they need to make this one.

Technical Debt Investment Case
═══════════════════════════════════════════════
Debt: Shared OrderService monolith
  
Current cost:
  Cycle time for Order-related stories: 8 days avg
  Cycle time for other stories: 3 days avg
  Overhead: 5 days per Order story
  Frequency: 4 Order stories per sprint
  Monthly overhead: ~40 engineer-days
 
Proposed investment:
  Decompose OrderService over 3 sprints (45 engineer-days)
  Estimated cycle time after: 3.5 days (reduction: 4.5 days)
 
Break-even: ~2.5 months after completion
12-month net saving: ~140 engineer-days
═══════════════════════════════════════════════

This is not always possible to quantify precisely. But even a rough estimate is better than no estimate. When you have no estimate, the conversation is subjective. When you have data, it is tractable.


The Only Rule That Matters

Pay the debt that is costing you more to carry than to fix. Leave the rest.

That sounds simple. It requires discipline. The discipline to leave imperfect code alone. The discipline to consistently allocate capacity for high-impact debt even under sprint pressure. The discipline to measure rather than guess which debt actually matters.

The teams that manage technical debt well do not have cleaner codebases than other teams. They have better developed intuitions about which messes are worth cleaning up. The rest of the time, they are shipping.