Experience & Methodology · consensus experience · recorded 2026-09-01

Problem: Tech debt keeps piling up — when you take over a "legacy mess", should you rewrite it or not? —— Trove problem library

Symptom

Let me start with the most expensive "tuition" I ever paid: a full rewrite.

I took over a legacy system that had been running for six years. The business logic was tangled into a knot — changing one field meant tracing seven or eight related spots. After three days of reading it, I was furious: "Why keep code like this?

Rewrite it!" I pulled in two colleagues, picked the hottest new framework at the time, and started from zero.

Three months later the new system ran — and then it hit us: six years of edge cases, hidden pitfalls, and compatibility logic were all buried inside that "garbage code", and the new system carried none of it over.

In the first month after launch, the business side filed over a hundred "this used to work, now it doesn't" tickets.

We spent another half year patching the old system's logic back in, piece by piece. That rewrite cost three times as much as writing it fresh — and it almost killed the product line.

This is not just my story. I have since watched too many teams fall into the same pit: take over a legacy system, get more and more annoyed, everyone chants "rewrite", plan it, schedule it, grind for three months — then get slapped by reality.

There is also the opposite failure: a friend's company treated the old system like a "family heirloom", afraid to touch anything.

Every requirement had to take a detour, and half a year later the business finally abandoned the system — the rewrite cost had grown tenfold.

If any of this sounds familiar — your code keeps getting messier, and you are spending every day tiling over a pile of legacy junk; or you just inherited an old project and your head is full of "let's just rewrite it" — this article is written for you.

It is not a refactoring tutorial.

It is the consensus of people who actually paid down tech debt in real projects and also stepped on the rewrite landmine: tech debt is not paid off by one big rewrite. It is paid off by many small repayments.

Cause

First, let's pin down what "tech debt" actually is. The term was coined by Ward Cunningham (the father of the wiki) in 1992: borrowing is not the problem; not paying the interest is.

Think of non-ideal code, design, and architecture decisions as the "principal", and the extra time spent maintaining, understanding, extending, and fixing them as the "interest" — it works exactly like financial debt: it feels great to borrow (fast), interest accrues every day (harder and harder to change), and you never know when it will blow up (change one line, break the whole world).

How does tech debt get created? Not because developers are lazy. Looking back at the pits I and the teams around me stepped into, debt almost always comes from "decisions that looked reasonable at the time":

  1. Crunch mode: "Just write it this way for this release, we'll refactor after launch" — after launch nobody remembers, and whoever remembers doesn't dare touch it;
  2. Requirements shifted: the code was designed for scenario A, the business became scenario B, and patches piled up on top of the original architecture, getting thicker and thicker;
  3. Turnover: the person who wrote it left, the comments and the code tell different stories, and everyone after them can only guess;
  4. Slack tests: no automated tests, so changing code feels like walking on thin ice — so people simply stop changing it, and the debt snowballs;
  5. Stale tech: dependencies stuck on years-old versions, and upgrading is impossible because nobody remembers why those versions were chosen.

Here is the most brutal mechanism of all: tech debt interest is compound interest, and you do not control when it gets collected. For the first six months you may feel nothing — the code even looks "fine".

Once the system grows to a certain size, every change starts to ripple through everything.

You spend 50% of your day fixing bugs instead of building features — by then the debt is too big to "dare to pay": refactoring's ROI is zero in the boss's eyes because "if it ain't broke, don't fix it".

But the moment the system collapses, the boss decides it's your fault for not maintaining it.

What I have observed over and over: the biggest obstacle to paying down debt is never technology — it is the obsession with "paying it all off at once". A full rewrite is the most expensive, most dangerous, lowest-success-rate way to repay.

The approach that actually works is the strangler pattern — like a strangler fig, grow a new system beside the old one, little by little, until the old one is wrapped up and replaced.

Every experience below is that consensus applied to a different scenario.

Solution

Each experience below follows the same shape: phenomenon → practice → why. None of it is one person's opinion — these are patterns that recur across many independent practices.

Experience 0 (L1 one-line takeaway): Don't rewrite — strangle it. Repay in small steps, each step rollback-safe

Tech debt can only be repaid in batches, never in one shot.

Every "tear it down and start over" repayment plan has a pitifully low success rate; every "old and new coexist, replace gradually" plan is slower but stable. If you only read one thing: when you take over a legacy mess, don't curse it first — wrap the system in a test safety net, then use the strangler pattern to replace it one small piece at a time.

Experiences 1–5 below explain why.

Experience 1: Identify first — turn "it feels bad" into "a list"

Phenomenon: almost every team knows it has tech debt, but ask "where is the debt" and nobody can say. The state I see most often: "this system is bad" is a consensus, but "what exactly is bad, and what do we pay off first" is a blank.

The result: people who want to repay have nowhere to start, and people who don't want to repay have an excuse to delay.

Practice: make the debt visible first.

Three pieces: ① draw a system heatmap (a tech-debt scanner like SonarQube, marking complexity, duplicated code, and code smells per module);

② build a tech-debt list where every item records "where it is, why it is debt, and what happens if we don't repay it"; ③ sort with a four-quadrant matrix — high impact + high urgency first, low impact + low urgency queue up.

The core logic is one line: only when "feelings" become numbers and a list can the debt be "repaid".

Why: the scariest thing about tech debt is that it is invisible. It never errors, never alerts — it just makes every change you make slower and slower.

A list and a heatmap make the invisible visible, so repayment gets priorities, progress, and accountability — the boss can see ROI, and the team can see hope.

Experience 2: No full rewrites — the strangler pattern is the only repeatedly validated route

Phenomenon: this is the step with the highest failure rate, and I flipped on it myself (the three-month rewrite I opened with). The temptation is huge: a new framework, a new architecture, clean code — who doesn't want that?

But the reality is: six years of edge cases, compatibility logic, and hidden pitfalls in the old system are all buried in the "garbage code".

You don't know they exist when you rewrite — until the business side finds you one by one after launch. The famous rewrite disasters (Netscape's rewrite, the initial Healthcare.gov launch) all follow the same script.

Practice: use the strangler fig pattern (coined by Martin Fowler): write new features in the new architecture, migrate old features module by module to the new system, take one old module offline for every one migrated, until the old system is fully "strangled".

Key principle: the old and new systems talk through an anti-corruption layer (Adapter), and until the old system is dead, it remains the source of truth. Order migrations by "business value × risk": migrate independent, low-risk, high-frequency modules first to build confidence, then touch the core.

Why: the strangler pattern splits "one big bang" into "countless small bangs", each with a contained, rollback-safe, verifiable blast radius. Business never stops, the boss never panics, the team gets positive feedback.

A full rewrite is essentially "betting three months of fun now against three years of risk later", while the strangler pattern is "trading small steps every iteration for certainty without a crash".

Experience 3: The test safety net — no tests, don't touch core logic

Phenomenon: the biggest fear in refactoring is "what if I break it". In a system without tests, changing one line relies on "feelings" and "luck"; after the change ships, if something breaks you can't even tell which change caused it.

I've watched too many teams die on the "refactor first, add tests later" ordering — mid-refactor the behavior changes, they don't know whether they broke it, they can only roll back, and after a few rounds of that, morale is gone.

Practice: add tests first, then refactor. You don't need full coverage — wrap the "critical paths" first: core business logic and high-risk code around money/permissions/data consistency.

Freeze current behavior with tests (even if the current behavior is wrong, freeze it anyway, because that is the production behavior).

A reference line that works in practice: get core-code coverage to 80%+ before touching it, and at least wrap the critical paths of high-risk code. Run tests after every step of the refactor — red → green → refactor loop.

Why: the test safety net is the precondition for "daring to touch". With it, refactoring goes from "gambling" to "verifying" — when you break something, the tests tell you, not the users after launch.

It is also the foundation that lets the strangler pattern run: without tests, you can't even prove "this module migrated over with identical behavior".

Experience 4: Small-step refactoring — each change small enough to roll back anytime

Phenomenon: the second common reason refactors crash is "too big a step". I saw a colleague refactor 2,000 lines in one go, couldn't even compile afterward, couldn't bear to roll back, and ended up fixing it overnight until the night before release.

The empirical rule: refactoring difficulty is not linear in change size — it is exponential. Every extra logic branch combinatorially raises the cost of understanding.

Practice: keep each refactor at a rollback-safe granularity (industry consensus: no more than 50 lines per change, and never touch an MR/PR larger than 500 lines).

Before each refactor, commit a clean baseline; right after the change, run tests + have a colleague review; only when CI is green do you commit the next step.

Don't buy "this change is just this big, it can't be split" — if you can't split it, your design abstraction is insufficient; do architecture adjustments first, then touch the code.

Why: small steps reduce the cost of "I broke it" from "roll back all night" to "revert one commit".

And there is a hidden benefit: every step gets reviewed by a colleague, so knowledge flows through the team — one refactor and the whole group understands that code better, something a big-bang refactor can never deliver.

Experience 5: Schedule debt repayment into iterations — debt is repaid, not waited for

Phenomenon: the debt list is built, priorities are set — now what? The most common ending is the list collecting dust, because "repaying debt" never makes it into the iteration and always gets squeezed out by new requirements.

"We'll pay it back when we have time" means never.

Practice: make debt repayment a fixed item in every iteration: reserve a fixed share of time (reference practice: around 20%) specifically for paying down debt. Repayment tasks get acceptance criteria, review, and records just like feature tasks.

Change "repaying debt" from "extra work" to "regular work".

The line that works with the boss: paying down debt isn't burning money, it's buying insurance for the system — skip today and the interest compounds every day.

Why: tech debt is generated continuously (every crunched requirement creates new debt), so repayment must also be continuous.

A fixed, institutionalized share means repayment doesn't depend on someone being "conscientious" or a release "happening to have slack".

It is the only way to outrun compound interest.

How to start (checklist, L1 execution layer)

  1. Scan before you act: generate a tech-debt heatmap with SonarQube (or similar), build the list, sort with the four-quadrant matrix;
  2. Add tests to critical paths: wrap core business + high-risk code first, reach 80% coverage before touching core logic;
  3. Pilot the strangler pattern on one independent module: write new features in the new architecture, migrate old modules one by one, isolate with an anti-corruption layer;
  4. Commit small: ≤50 lines per refactor, CI green + review at every step, always rollback-safe;
  5. Schedule repayment into iterations: a fixed 20% of every iteration, with acceptance criteria, records, and review.

How to apply this in your situation (L3 deep layer)

  • Solo projects: there is no boss or team — your creditor is yourself. Run a scanner over your code regularly, and at minimum add tests to every module where "every change is a guessing game"; commit a clean baseline with git commit before refactoring so you can always fall back.
  • Small teams (5–15 people): build the tech-debt list and reserve a fixed share of every iteration for repayment; skip "refactoring weeks" — centralized repayment hurts the business and tends to fizzle out; steady drip is the real answer.
  • Large systems / core business: strangler pattern only, anti-corruption layer for isolation, migration order by business value × risk; for any Big Bang rewrite proposal, ask three questions first: have you inventoried all of the old system's edge cases? is the test safety net in place? can the business absorb months of instability? If you can't answer, don't greenlight the project.
  • The AI era: AI is a great refactoring assistant (detecting code smells, batch replacements, generating tests) — but the "rewrite" temptation is even bigger now: AI can generate a new system so fast that you mistake rewrite costs for cheap. Stay sober: what AI writes for you is also new code, and it can't see the old system's business logic either. Let AI be the "scalpel" in the strangler pattern — not the "bulldozer".
  • If you are hesitating over "should we rewrite?": do Experience 1 and 3 first — list the debt, add the tests. After those two steps, you will most likely find that the part that truly needs "rewriting" is far smaller than you imagined; the rest of the debt, the strangler pattern can pay off just fine.

(Final words) My biggest mistake was believing "once we rewrite it, everything will be clean" — two months in, the legacy system's edge cases started fighting back in week four, and I rolled everything back in shame.

The lesson: technical debt is not dirt to be removed in one sweep; it is interest you owe the future, and it has to be paid monthly. Skip the big cleanup — write the debt list, add the tests, and let the strangler pattern grind the rest.

You do not need to pay it all off at once; you only need to start paying today.

Tool-agnostic

This is consensus experience, not bound to a specific tool; tool-bound problems live on their tool pages.

Source & evidence

Recorded consensus experience · not locally measured
Consensus experience: not locally verified; cases are desensitized into the body; external sources are not listed individually.

← Experience & Methodology domain | Problem Catalog Experience | Facing another issue? Go to the Helpdesk