Refactor or Rewrite
Five questions decide it: can the behaviour be characterized, can the change be incremental, is the architecture salvageable, how much undocumented domain knowledge is in there, and can old and new run side by side.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
This code is genuinely bad and the team wants to start again. What would have to be true for that to be the cheaper path, and what does it cost if I am wrong?
The billing engine is eight years old, has no tests above the unit level, and every change takes a fortnight. Two engineers want six months to rewrite it. The business wants three billing features this quarter.
The code is beyond saving. A clean implementation with the knowledge we have now will take six months and everything afterwards will be faster.
The estimate prices building the new system and not the thing that actually consumes the time: discovering what the old one does. Eight years of unwritten rules — a rounding convention, a grandfathered discount, a partner with special terms — are in the code and nowhere else (What "Legacy" Actually Means).
- The estimate prices building the new system and not the thing that actually consumes the time: discovering what the old one does. Eight years of unwritten rules — a rounding convention, a grandfathered discount, a partner with special terms — are in the code and nowhere else (What "Legacy" Actually Means).
- While the rewrite runs, the old system keeps changing, because the business does not stop. The new one is now chasing a moving target, and the gap grows with every quarter the rewrite slips.
- Six months is the estimate for a system whose requirements are believed to be understood. Rewrites of systems where the requirements are *in the code* routinely take several times their estimate, and the overrun is discovered late.
- And the cutover is the risky part, not the building. A rewrite that cannot run alongside the old system has one moment where everything moves, and that moment is a billing night (Incremental Migration).
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- Billing runs every night and cannot stop. Whatever happens, invoices go out on the first of the month.
- Nobody currently on the team wrote the original, and the two people who did have left.
- The correct behaviour is defined by what the system does today, including the parts everyone assumes are bugs — some of those are contractual.
- Every customer is billed exactly what they were billed before, unless a requirement said otherwise and someone signed off on the difference.
- At no point is there a period where invoices are produced by neither system or by both.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Whoever proposes a rewrite owns answering the five questions in writing, and owns the migration plan — not as a final phase, but as the first design constraint.
- The existing system owns being the specification until something else demonstrably reproduces it (Characterization Tests).
- The business owns the risk decision, which means it has to be presented as a risk decision rather than as a technical one.
- Whether old and new can coexist decides almost everything. A seam that lets one customer, one product line or one invoice type run on the new path turns a single dangerous event into a series of small reversible ones (The Strangler Pattern).
- If no such seam exists, finding or building one is the first piece of work regardless of which path is chosen (Finding Seams).
Five questions, and what the answers point at
The decision is not binary and it is not primarily about code quality. Four outcomes are available, and the five questions sort between them faster than any amount of discussion about how bad the code is.
Two of the questions are worth more than the others. Whether behaviour can be characterized decides whether any change is safe; whether old and new can coexist decides whether the migration is a series of reversible steps or a single event (Incremental Migration).
Can behaviour be characterized, can changes be incremental, is the architecture salvageable, how much hidden domain knowledge is there, and can old and new coexist?
when Behaviour can be pinned by tests, the architecture is messy but not wrong, and there are seams to work with. The problem is entanglement rather than the model.
cost Slow and unglamorous, and it competes with features every sprint forever. Risk is low and reversible: each step is small and verified. The failure mode is that it is never funded and the code stays exactly as it was (The Refactoring Loop).
when Old and new can coexist, and traffic can be routed per customer or per product line. The model is wrong for part of the system but the boundary is findable.
cost Two implementations, a routing layer, and a long period of doubled load — plus the discipline to actually retire the old path, which is where most of these stall (The Strangler Pattern).
when One component — the proration calculator, the tax module — is genuinely beyond repair, and it sits behind an interface the rest of the system already uses.
cost The interface has to be honest, and it frequently leaks the old model, so the new component inherits the shape it was meant to escape. Highest value per unit of risk when the seam is real (Extract Module).
when The core model cannot express something now required, the runtime is end-of-life, or the specification exists outside the code and is trustworthy — and old and new can be run in parallel and reconciled.
cost No features for the duration, an estimate with a poor track record, and a two-system state if it stalls. Only defensible with a written migration plan, a parallel-run period, and a reversible cutover (The Risk in a Rewrite).
when The system is stable, changes are rare, and the fortnight-per-change is annoying rather than blocking.
cost Nothing now, and the option expires: hidden knowledge continues to leave with people, and the same decision costs more each year (When Design Does Not Pay).
The questions, scored honestly for this system
Answering the five questions in a table takes an hour and settles most of the argument, because the answers are facts about the system rather than opinions about the code.
The point of writing them out is that a rewrite requires favourable answers on several at once. One bad answer — no way to run old and new together — is usually enough to remove the full-rewrite option entirely, whatever the code looks like.
| Question | Answer for this billing engine | What it rules in or out |
|---|---|---|
| Can behaviour be characterized? | Partly. Invoice output is observable and replayable from production data; internal state is not | Makes any path viable, and makes the first task obvious: replay a month of invoices before touching anything (Characterization Tests) |
| Can changes be incremental? | Yes for new product lines, no for the proration core, which every path goes through | Points at a partial rewrite of proration behind the existing interface rather than at a whole-system replacement |
| Is the architecture salvageable? | Messy but not wrong: there is a real domain model under the procedural code, and the seams exist even though nothing uses them | Strongly against a full rewrite. The expensive thing to rebuild is the model, and the model is fine (Finding Seams) |
| How much hidden domain knowledge? | High. Eight years of grandfathered terms, at least three of which are contractual and undocumented | The dominant cost in any rewrite, and the reason the six-month estimate is unreliable (What "Legacy" Actually Means) |
| Can old and new coexist? | Yes — invoices can be generated by both and reconciled before sending, per customer | The single most favourable fact here. It makes incremental replacement genuinely safe and makes any cutover reversible (Expand and Contract) |
What each path costs the quarter after next
The comparison that decides this is not "six months versus forever". It is what the third billing feature costs under each path, and when the business gets it.
Both columns below are real outcomes, and the one that looks worse on total effort is the one with the lower variance. That is usually the trade being made, and it is worth saying out loud rather than arguing about code quality.
Usage-based pricing for one product line, a new tax jurisdiction, and per-customer invoice scheduling.
All three features wait for the rewrite. If it lands on time they are cheap afterwards; if it slips two quarters — the common case where domain knowledge has to be rediscovered — they are delivered a year late, or built twice, once in each system to keep the business moving.
Two of the three features are built on the new path and delivered this quarter; the tax change is made in the old engine because that is where tax lives, and is migrated later. Every step is reversible by flipping a route.
How to build it
Most important first.
- Answer the five questions before comparing timelines: can behaviour be characterized; can the change be made incrementally; is the architecture salvageable; how much hidden domain knowledge is in there; can old and new coexist.
- Characterize first, whichever way it goes. Tests that pin current behaviour are needed by a refactor to be safe and by a rewrite to be verifiable, so this work is never wasted (Characterization Tests).
- Prefer incremental replacement when a seam exists. It is slower in total effort and dramatically cheaper in risk, because every step is reversible and the business keeps getting features (The Strangler Pattern).
- Where a rewrite is chosen, run both systems in parallel against real traffic and compare outputs before switching anything. In billing that means shadow-running a month and reconciling every invoice (Expand and Contract).
- Scope it to the part that is actually bad. Partial rewrites of one subsystem, behind an existing interface, capture most of the benefit at a fraction of the risk (Extract Module).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Refactor with characterization tests: the three features this quarter still cost a fortnight each at first, and the fourth costs a week, and the eighth costs days. The improvement is gradual and it is visible in delivery rather than in a milestone.
- Incremental replacement: features can be built on the new path as soon as the first slice is live, so the business gets value during the migration instead of after it. Total effort is higher and total elapsed time is longer.
- Full rewrite: zero features for the duration, then a step change — if it lands. The cost of being wrong is not the six months, it is the two-system state, where every change is made twice, forever, until one is finally retired.
- What none of them changes: the essential complexity of eight years of billing rules. Whatever is built has to state all of them (Essential and Accidental Complexity).
- Incremental replacement means living with two implementations for a long time, with a routing layer and doubled cognitive load, and teams genuinely burn out on it.
- Refactoring under a business that will not fund it produces a permanent half-state, which is worse than either endpoint.
- A full rewrite is occasionally right — a system on a runtime that is end-of-life, or one whose data model cannot express a legally required concept — and refusing it on principle is its own failure mode.
What can go wrong
- The rewrite reaches eighty percent and stalls, and the organisation now maintains two systems — the most expensive outcome available and a common one.
- The new system reproduces the old behaviour faithfully, including the bugs it was supposed to fix, because the characterization tests encoded them as requirements. This is usually correct and always disappointing.
- The refactor path is chosen, is never given time, and the code stays as it was — a decision made by omission rather than by argument (What Technical Debt Actually Is).
- A partial rewrite is scoped behind an interface, and the interface turns out to leak the old system's model, so the new code inherits the shape it was meant to escape (Leaky Abstractions).
- A rewrite depends on the requirements being knowable from somewhere other than the code. Where they are not, the old system is a dependency of the new one for as long as the migration lasts.
- Incremental replacement depends on a seam and on the ability to route traffic per unit — customer, tenant, product (Feature Flags and What They Cost).
- Both depend on the team's capacity to keep the old system running while the work happens, which is the constraint most often ignored in the estimate (Bus Factor).
- "A rewrite is cleaner." The new code is cleaner for the requirements you remembered. The cost is not code quality, it is that the old system is the only complete statement of the requirements, and reproducing it is the work (The Risk in a Rewrite).
- "Incremental means slower." Incremental is slower to a *finished* replacement and faster to the first delivered value, and it keeps delivering throughout. Which one matters depends on whether the business can go two quarters without billing features.
- "We rewrote it and it went fine." Sometimes it does — usually when behaviour was well specified elsewhere, the system was small, or old and new could run in parallel. Those are the conditions, and they are checkable in advance (The Risk in a Rewrite).
- "The architecture is unsalvageable." Sometimes true, and it is one of five questions rather than the whole answer. Bad architecture with good seams is more tractable than good architecture with none (Finding Seams).
- shotgun-surgery
- god-object
Testing it, and how it ages
- Characterization tests at the outermost boundary — same inputs, same invoices, byte for byte. Start with production data replayed, not with hand-written cases (Characterization Tests).
- Reconciliation, not assertion, for the parallel-run phase: run both, diff every invoice, and treat each difference as a question rather than a failure. Some differences will be intended.
- Test the rollback path. A cutover you cannot reverse is not a cutover, it is a launch (Designing the Migration).
- Systems that were rewritten once are usually rewritten again, because the conditions that produced the first mess — no tests, no seams, undocumented rules — are cultural and are rarely what the rewrite fixed.
- The incremental path leaves a system with seams, which is what makes the *next* modernisation cheap. That compounding benefit is the strongest argument for it and the hardest to put in a business case.
- A rewrite's value decays with the time it takes: a system rebuilt over three years is built against requirements from three years ago (Requirements Are a Snapshot).
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThe five questions apply to any system that is running and has users; what varies is which question dominates. For a data-heavy system it is coexistence, because the data cannot be in two shapes at once; for a rules-heavy system it is hidden domain knowledge.
- DOMAIN-SPECIFICIn billing, tax and clinical systems the hidden-knowledge question dominates everything else, because the accumulated exceptions are contractual or legal and cannot be rediscovered by reasoning. In a stateless rendering service with a clear specification, the same question is nearly free and a rewrite is a far more reasonable proposition (When Design Does Not Pay).
- CONTESTEDThe strongest case for rewriting, which the incremental camp tends to caricature: when a system's core model is wrong — it cannot express a concept the business now requires — incremental change does not converge, it accretes adapters around the wrong model, and each increment makes the eventual change more expensive. Teams that have spent three years strangling a system that should have been replaced in one are entitled to be sceptical of "always incremental". The honest position is that the argument turns on whether the model is wrong or merely messy, and that this is genuinely hard to tell from inside.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — System Design — the coexistence question is the one that most often decides a rewrite at system scale too, and there it is usually settled by whether the data can be dual-written rather than by anything about the code.