Engineering · 8 min read
How to pay down technical debt without stopping feature work
You pay down technical debt without stopping feature work by treating it as a standing line item, not a project. Measure where debt actually hurts, keep a ranked register, reserve a fixed share of every sprint, and replace risky parts incrementally behind tests, abstractions and feature flags. DORA metrics tell you whether it is working.
By BacklooPublished
Key takeaways
- Measure first: debt that sits in rarely touched code is cheap; debt in files that change every week and cause incidents is expensive.
- Keep a debt register with cost, risk and a named owner, and reserve a fixed slice of every sprint for it instead of negotiating each item.
- Replace systems piece by piece with the strangler fig pattern, branch by abstraction and feature flags. Big-bang rewrites are the riskiest option.
- Write characterization tests before refactoring, and use the DORA four keys as the scoreboard everyone can read.
Why a feature freeze is the wrong fix
A feature freeze trades a slow, visible cost (debt) for a sudden, visible one (no product progress), so it rarely survives the first board meeting. The approach that lasts is continuous: a small, predictable amount of debt work in every cycle, aimed at the code that hurts most, with results the business can see.
The cost is real. Debt shows up as slower delivery, more regressions, longer onboarding and on-call fatigue. It rarely shows up as a line in the budget, which is why it keeps losing to features.
10–20%
You are already paying that tax. The question is whether you pay it as interest, through slow and fragile delivery, or as principal, through planned work that makes the next change cheaper.
Measure debt where it hurts: hotspots and incidents
Measure debt by combining how often code changes with how hard it is to change. A complex file nobody touches is a curiosity. A complex file that changes every week is a hotspot, and hotspots are where debt turns into cost. Add incident data and you have a defensible priority list.
Churn × complexity
Churn comes straight from version control. Complexity can be as simple as lines of code or as precise as cyclomatic or cognitive complexity from your linter. Multiply the two and sort. The top ten files usually account for a large share of the pain your team already complains about.
# commits per file over the last 6 months (churn)
git log --since="6 months ago" --name-only --pretty=format: \
| grep -E '\.(ts|tsx|py|go)$' \
| sort | uniq -c | sort -rn | head -30
# rough complexity proxy: lines per file
wc -l $(git ls-files 'src/**/*.ts') | sort -rn | head -30Do not over-engineer this. A spreadsheet that joins the two lists is enough to start a useful conversation. Dedicated tools add trend lines and ownership maps once you know the exercise pays off.
Link debt to incidents
Every postmortem should answer one extra question: which part of the codebase made this worse or harder to fix? Tag the incident with the module. After a quarter, the modules with the most incident tags and the highest churn are your first targets, and you have evidence to show for it.
Keep a debt register, not a backlog graveyard
A debt register is a short, ranked list of known debt with an estimated cost, a risk level and an owner. It differs from a backlog full of “refactor X” tickets because each entry states what the debt costs today and what paying it down unlocks, so it can be prioritized against features in the same language.
| Field | Example | Why it matters |
|---|---|---|
| Item | Order pricing logic duplicated in API and admin | Names the problem, not the fix |
| Evidence | Top-3 hotspot; 2 incidents last quarter | Ties it to measurable pain |
| Interest | Every pricing change touches 2 codebases, ~1 extra day | The ongoing cost of doing nothing |
| Risk | High: wrong prices reach customers | Business impact if it fails |
| Proposed step | Extract a pricing module behind one interface | Smallest change that helps |
| Owner | Named engineer | Someone is accountable |
| Status | Next sprint | Visible progress |
Rank by interest × risk, not by how much the code offends you. Review the register once a month, retire items that no longer matter, and cap it at around twenty entries. A register nobody reads is just a second backlog.
Reserve a fixed capacity budget every sprint
The simplest rule that survives contact with deadlines is a fixed share of capacity reserved for debt in every sprint, agreed once with product and not renegotiated item by item. Pick a number you can sustain, for example one day in ten, and protect it like any other commitment.
- Fixed, not leftover. Debt work done “when there is time” never happens. Plan it first.
- Pulled from the register. The team takes the top items, so there is no debate about what counts.
- Boy-scout rule on top. Small cleanups in code you are already changing are part of the feature, not the budget.
- Flexible inside a quarter. Skip a sprint for a launch, then pay it back. Track the balance openly.
Replace, don't rewrite: strangler fig, branch by abstraction, flags
Large debt is paid down by replacing a system piece by piece while it keeps running, never by a big-bang rewrite. Three techniques do most of the work: the strangler fig for whole subsystems, branch by abstraction for code inside one codebase, and feature flags to switch traffic safely. If you are weighing a rewrite anyway, read rescue, refactor or rewrite first.
The strangler fig pattern
Martin Fowler named the pattern after a vine that grows around a tree until it replaces it. Put a routing layer in front of the old system, build the new implementation for one route or one capability at a time, and move traffic over as each piece is ready. The old code shrinks until it can be deleted. At no point is there a risky cut-over weekend.
Branch by abstraction
For code inside one repository, introduce an interface in front of the part you want to replace, move every caller to the interface, build the new implementation behind it, then switch and delete the old one. Everything ships to main continuously; there is no long-lived branch to merge at the end.
Feature flags as the switch
A flag decides at runtime which implementation serves a request. Roll out to internal users, then a small percentage, then everyone, and roll back by flipping the flag instead of redeploying.
// One interface, two implementations, one switch.
interface PriceCalculator {
quote(order: Order): Money;
}
const calculator: PriceCalculator = flags.isOn("pricing-v2", { userId })
? new PricingModule() // new, tested, owned
: new LegacyPricing(); // old path, deleted when the flag reaches 100%Flags are debt too. Give every flag an owner and a removal date, and delete it within a sprint or two of full rollout.
Put a safety net in place before you refactor
Refactoring without tests is just changing code and hoping. Before touching a hotspot, pin its current behaviour with tests, even if that behaviour is odd. Those characterization tests tell you the moment your change alters something users depend on.
- Characterization tests cover the main inputs and outputs of the module you will change
- At least one end-to-end test covers the critical user path through it (sign-up, checkout, booking)
- CI runs the tests on every pull request and blocks merges on failure
- Error tracking and basic metrics are live, so a regression in production is visible within minutes
- A rollback path exists: a flag, a revert or a previous deploy you can promote
If the codebase was produced quickly with AI tools and has almost no tests, start there. Our production checklist for vibe-coded apps covers the minimum safety net.
Use the DORA four keys as your scoreboard
The DORA four keys measure whether debt work is paying off, because they track delivery speed and stability together: deployment frequency, lead time for changes, change failure rate and time to restore service. If paying down debt is working, the first two improve and the last two fall.
| Metric | What it measures | How debt hurts it | Where to get it |
|---|---|---|---|
| Deployment frequency | How often you ship to production | Fragile code makes teams batch changes | CI/CD deploy logs |
| Lead time for changes | Commit to running in production | Slow reviews, flaky tests, manual steps | Git + deploy timestamps |
| Change failure rate | Share of deploys causing an incident or rollback | Coupled code breaks in unexpected places | Incident tracker + deploys |
| Time to restore service | How long recovery takes | Poor observability, risky rollbacks | Incident timestamps |
Record a baseline before you start, review the four numbers monthly, and resist turning them into individual targets. They describe the system, not people.
Explain debt to non-technical stakeholders
Explain debt in the currency stakeholders already use: time to market, risk and money. Nobody outside engineering needs to hear about coupling or test coverage. They need to hear that pricing changes take a week instead of a day, and that the last two outages came from the same module.
- Name the business symptom. “Every pricing change takes five days and broke checkout twice this quarter.”
- State the interest. “That costs roughly one engineer-day per change, plus incident time.”
- Propose a bounded step. “Two sprints at our normal debt budget to extract the pricing module.”
- Promise a measurable result. “Lead time for pricing changes under two days, tracked on the DORA dashboard.”
- Report back. Show the before and after. Trust earned here funds the next item.
Debt work that cannot be explained as a faster or safer product is usually not the debt to pay first.
How Backloo can help
If your team is stretched, an outside engineer can run the measurement, build the first register and do the riskiest refactors while your team keeps shipping. That is the core of our maintenance and SLA care plans: a fixed monthly budget for updates, monitoring and planned debt work.
For larger structural problems, such as a backend that has outgrown its first design, we plan a strangler-fig migration as part of backend systems work. Not sure where to start? A Rescue Audit gives you a ranked, written view of the debt, and you can always talk to us first.
Questions
How much sprint capacity should go to technical debt?
There is no universal number. Pick a fixed share you can sustain every sprint, such as one day in ten, protect it, and adjust after a quarter based on the DORA metrics and incident trend.
Is a full rewrite ever the right call?
Sometimes, when the platform is unsupported or the data model is fundamentally wrong. Even then, an incremental replacement with the strangler fig pattern is usually safer than a big-bang cut-over. Make the case in writing with evidence before committing.
What is the difference between a debt register and a backlog?
A backlog lists work. A debt register lists known debt with its evidence, ongoing cost, risk and owner, so each item can be ranked against features in business terms. It stays short and is reviewed monthly.
Which tools do I need to measure technical debt?
To start, only git history, your linter and your incident tracker. Joining churn, complexity and incident tags in a spreadsheet is enough to find the first hotspots. Add dedicated tools once the habit sticks.