Skip to content

Insights · Engineering · Feb 1, 2025 · 8 min read

Technical debt honestly: when to refactor and when to rewrite

Technical debt is a trade, not a failure. Here is how it accumulates, a plain framework for choosing refactor versus rewrite, how strangler-fig migrations work, and how to budget paydown alongside features.

build-orchestrator

main

src/

queue.ts

pool.ts

retry.ts

types.ts

index.ts

export interface BuildJob {

repo: string;

region: "ca-west-1";

}

export async function queue(

job: BuildJob,

): Promise<Run> {

return await pool.add(job);

212

Tests passed

0

Type errors

1.4s

Build time

Watching src/ — optimised rebuild on save

Technical debt is the future work you accept when you ship code that is cheaper now but costlier to change later. Most of the time the right response is disciplined, incremental refactoring rather than a rewrite; a full rebuild is justified only when the architecture itself blocks what the business needs next and a strangler-fig migration cannot get you there safely. The honest part is treating remediation as a standing budget line in every cycle, not a someday project that competes with features and always loses.

Key takeaways

  • Technical debt is a trade, not a moral failing. The problem is unmanaged debt, not debt itself.
  • Debt accumulates through deadlines, dependency drift, staff turnover and product pivots, not just careless code.
  • Default to refactoring in place. Reach for a rewrite only when the architecture blocks the roadmap and incremental change cannot fix it.
  • Strangler-fig migrations replace a system piece by piece behind a stable boundary, so you have a working product at every step.
  • Budget remediation as a fixed share of every development cycle, and spend it in the parts of the code you change most often.

What is technical debt, really?

The metaphor comes from Ward Cunningham: shipping imperfect code is like taking a loan. You get to market sooner, and in exchange you pay interest, in the form of extra effort, on every future change that touches that code. Pay the principal down through refactoring and the interest stops. Let it ride and the interest compounds until routine changes feel like surgery.

Two distinctions keep the conversation honest. First, debt is not the same as bad code. A team that knowingly ships a simple version to test a market, with a plan to revisit it, has taken prudent, deliberate debt. A team that does not know what a layered design is has taken reckless, inadvertent debt. Both cost you, but they call for different fixes: one needs follow-through, the other needs skills and review practices.

Second, debt only matters where you pay interest. A crude module nobody has touched in three years is a cosmetic issue. A crude module in the middle of your checkout flow, edited every sprint, is a genuine liability. This is why line-count metrics and blanket "code quality" scores mislead: the cost of debt is the extra time per change multiplied by how often you change it. That simple formula, applied honestly, tells you where to spend.

How does technical debt accumulate?

Almost never through one bad decision. Debt builds the way sediment does, in thin layers that each looked reasonable at the time.

  • Deadline pressure. The temporary workaround before a launch becomes load-bearing, because nothing is more permanent than a fix that works.
  • Dependency drift. Frameworks, runtimes and libraries move on. Every skipped upgrade is quiet borrowing, and the migration gets harder the longer you wait.
  • Product pivots. The data model that fit the original product bends to fit the new one. Each bend is small; together they leave names, tables and abstractions that no longer describe what the software does.
  • Turnover. When the people who understood why the code is shaped that way leave, their successors work around what they do not understand, and the workarounds become new debt.
  • Missing tests. Without a safety net, nobody refactors, because nobody can prove they have not broken anything. Debt that cannot be paid safely simply grows.

The compounding is what hurts. Slow changes lead to bigger, riskier releases, which lead to more hotfixes, which add more debt. The end state is recognisable: estimates balloon, deploys are feared, and good developers start to leave. By then the debt is a business problem wearing an engineering costume.

How do you decide between refactoring and rewriting?

Start from a principle: the running system is the only complete specification you have. Years of bug fixes, edge cases and regulatory quirks live in that code and nowhere else. A rewrite discards that record and promises to rediscover it. So the burden of proof sits on the rewrite, always.

Four questions carry most of the decision:

  1. Where does it hurt: code or architecture? Messy functions, duplication and poor naming are code problems; refactoring fixes them in place. A wrong data model, a framework at end of life, or a monolith that cannot scale the one part that needs to scale are architectural problems, and refactoring alone may not reach them.
  2. Do you understand current behaviour? If the system has tests, or you can add characterisation tests around the areas you touch, incremental change is safe. If nobody can say what the system does, a rewrite is even more dangerous, because you would be rebuilding from guesses.
  3. Can you keep shipping while you fix it? Any plan that freezes features for months while a new system catches up tends to fail: the old system keeps moving, the new one chases it, and the gap never closes.
  4. What does the business need in the next couple of years? Debt is only worth paying where it blocks that path. A platform you plan to retire deserves patches, not renovation.
ApproachBest fitRisk profileTime to visible value
Incremental refactoringCode-level debt in a sound architectureLow, changes are small and reversibleFast, improvement lands with each release
Strangler-fig migrationArchitectural debt in a system that must keep runningModerate, managed at each cutoverSteady, value arrives slice by slice
Targeted module rewriteOne well-bounded component that is clearly wrongModerate, contained by the boundaryMedium, value lands when the module ships
Full rewritePlatform truly at end of life, small or short-lived codebaseHigh, big-bang cutover and second-system temptationSlow, little value until the end

The pattern in that table is not subtle. The bigger the bang, the longer you wait for value and the more ways the plan can fail. Full rewrites earn their place only at the extremes: a genuinely small system, a platform being switched off by its vendor, or a product changing so completely that the old behaviour no longer matters.

A rewrite is not a fresh start. It is a promise to rebuild everything you have learned, without breaking anyone who relies on it.

What is a strangler-fig migration, and when does it work?

The strangler fig is a tree that grows around a host until it stands on its own. Applied to software, the pattern means you place a stable boundary, often a routing layer, API gateway or facade, in front of the legacy system, then build replacement capabilities behind that boundary one slice at a time. Each slice takes over its share of real traffic. The old code keeps handling everything else until, path by path, there is nothing left for it to do.

The discipline is that both systems stay live throughout. You can compare the new slice against the old one on identical requests before trusting it, roll back a single slice instead of a whole launch, and stop the migration at any point with a working product. Feature development continues, because new work is simply built on the new side of the boundary.

The hard part is almost always data. While both systems run, they must agree on state, which means choosing a source of truth per domain and handling synchronisation deliberately, whether through shared storage, change data capture or events. Slices should follow business capabilities, not technical layers, so each one delivers something a user or stakeholder can see. Teams doing this as part of a broader full-stack modernisation often pair it with a move to managed cloud infrastructure, since standing up the new slices on modern hosting avoids reproducing the old operational constraints.

When does it not fit? When the codebase is small enough that a careful refactor is simply cheaper, or when the data is so entangled that no clean boundary exists yet. In the second case the first project is not migration at all: it is refactoring the legacy system just enough to create the seam you will migrate through.

How do you budget debt paydown alongside features?

The approach that fails is the "debt sprint": save everything up, then ask the business to pause features for a quarter while the team cleans up. It is a hard sell, it delivers nothing visible, and the backlog refills the moment it ends. Sustainable paydown looks less dramatic.

  • Reserve a fixed share of every cycle. Decide with product leadership what fraction of capacity belongs to remediation, and defend it the way you would defend payroll. The exact share matters less than its consistency.
  • Keep a debt register, ranked by interest. A short, honest list of known debts, each with a note on which planned work it slows down. Ranking by friction, not by offence to taste, keeps the list credible.
  • Pay debt where you are already working. Attach remediation to feature work in the same area, and leave each file a little better than you found it. Refactoring code you will touch again next month repays immediately; polishing a dormant corner repays never.
  • Make the cost visible in planning. When an estimate is doubled by debt, say so in the estimate. Product decisions improve quickly once the interest shows up on the invoice.

Framed this way, remediation stops being a fight between engineering and product and becomes a shared portfolio decision: some capacity buys new capability, some buys back speed.

How OlDevs helps you pay it down

OlDevs is a full-stack technology studio in Vancouver, building and modernising software since 2014. Modernising existing systems is a regular part of that work: assessing where a codebase actually hurts, recommending refactor, strangler-fig or rewrite with the reasoning written down, and then carrying out the plan with a working demo every week so progress is never a matter of faith. One accountable team handles the boundary layer, the new services and the data migration, and you own all code, designs, accounts and IP from day one. If you would like a second opinion before committing to a big decision, that is a conversation we are glad to have; our engagement process starts with understanding the system you have, not the one we would like to sell you. Request a quote and we will reply within one business day.

FAQ

Questions on this topic.

Not always. Debt taken deliberately, with a plan to repay it, is a normal trade for speed to market. It becomes a problem when it goes unmanaged: interest compounds in the code you change most often, estimates grow, releases get riskier, and the cost stays invisible to the people setting priorities.

When the pain is architectural rather than code-level, when incremental refactoring cannot reach it, and when you can keep shipping during the transition. Even then, a strangler-fig migration is usually safer than a big-bang rewrite, because the system keeps working at every step and each slice can be rolled back.

Reserve a consistent share of every development cycle for remediation, keep a short debt register ranked by how much friction each item causes, and spend the budget in the areas of the code you are actively changing. Paydown attached to feature work repays immediately; isolated cleanup sprints rarely survive contact with a roadmap.

Still have a question? Ask us when you request a quote

Let’s connect

Want this applied to your business?

Tell us what you’re building. We’ll reply within one business day with next steps and a tailored quote.

We’ll only use your details to prepare your quote. No lists, no spam.

Call us Request a quote