LegacyGENERALSCALE-SPECIFICCONTESTED

The Risk in a Rewrite

Rewrites fail for four specific reasons — hidden requirements, the delivery gap, migration complexity and feature-freeze pressure. They are sometimes still the right call, and the conditions are nameable.

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.

The question

What exactly goes wrong in a rewrite, and under what conditions is one actually the better bet?

The requirement

The team proposes replacing a seven-year-old fulfilment service. The argument is that it is unmaintainable, that the framework is two major versions out of support, and that a fresh implementation would take four months.

The obvious build

The code is a mess, we understand the domain now, and we would build it properly this time. Four months, and then everything is easy.

Why it breaks

The estimate is made against the requirements the team can *see*. The old system also implements the ones it cannot: the retry that a partner integration depends on, the field a report reads, the rounding that keeps the ledger balanced. Those are discovered after cutover, one escalation at a time (The Requirements Nobody States).

How it breaks as requirements change
  • The estimate is made against the requirements the team can *see*. The old system also implements the ones it cannot: the retry that a partner integration depends on, the field a report reads, the rounding that keeps the ledger balanced. Those are discovered after cutover, one escalation at a time (The Requirements Nobody States).
  • Four months of no delivery is four months in which the business either waits or asks for the feature in the old system anyway — and once it does, the rewrite is chasing a moving target it can never catch.
  • The rewrite plan is a plan for the new code. It usually is not a plan for the data, and moving seven years of orders into a different model is frequently larger than the code work (Data Migration).
  • To stop the target moving, someone proposes a feature freeze. Freezes hold for about six weeks, then an urgent customer commitment breaks it, and now both systems must implement the change.
  • As the deadline slips — and it does, because the discovered requirements were not estimated — the pressure becomes to cut scope from the new system, which means shipping something that does less than the thing it is replacing.
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

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.

Constraints
  • The old system must keep running and keep receiving urgent fixes for the entire duration of the rewrite.
  • The framework really is out of support, so "do nothing" has a security cost that grows and is not zero.
  • Nobody currently at the company can state the full behaviour of the old system; the last author left in 2021.
  • The team is nine engineers, and a rewrite consumes most of them, so feature delivery is the currency being spent.
Invariants
  • Behaviour that customers or downstream systems depend on must survive — including behaviour nobody has written down, which is the part that makes this hard.
  • Data written by the old system must remain readable and correct, before, during and after (Data Migration).
  • Whatever is decided, there must exist a state the team can stop in. A plan with no safe abort point is not a plan.

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • Whoever proposes the rewrite owns naming what specifically cannot be achieved incrementally. "It is a mess" is not that; "the data model makes multi-warehouse impossible and every incremental path has to migrate the same tables anyway" is.
  • Someone owns the behaviour inventory: the list of things the old system does that the new one must do, and how each was established.
  • Someone owns the migration of data and downstream consumers as a first-class deliverable with its own estimate, not as a phase at the end.
  • Someone owns the abort criteria, written before starting, so that stopping is a decision rather than an admission (Revisit Triggers).
Boundaries
  • The boundary that decides everything is the one between "we can replace this behind an interface" and "this is entangled with the data model and the schema is the problem". The first is a strangler; the second is where rewrites become genuinely competitive (The Strangler Pattern).
  • Scope the rewrite to a bounded thing that can be cut over independently. A rewrite of a whole platform has no boundary at all and therefore no safe abort point.
  • Downstream consumers reading the old system's data directly are outside your boundary and inside your problem.

The four mechanisms

Rewrites do not fail because engineers are optimistic in general. They fail through four specific mechanisms, each of which is visible in advance and each of which has a mitigation that costs time — which is why they are so often skipped by a plan that was sold on speed.

Read the table as a risk register for the proposal on the table. A rewrite that has an answer for all four rows is a much better bet than one that has an answer for none, and the difference is usually two months of schedule.

Why rewrites fail, and what actually addresses each
TriggerSymptomCauseResponse
Cutover, then week threeA stream of escalations about behaviour nobody knew existed — a retry, a rounding rule, a field a report readsHidden requirements: the old system implements things the ticket history never recordedBuild a behaviour inventory from production traffic and data before estimating; characterize the old system as if reimplementing from it (Characterization Tests)
Month two of a four-month planThe business asks for a feature; it is built in the old system; the new system is now behind againThe delivery gap — a rewrite produces nothing shippable for its whole durationCut over slice by slice so value lands continuously, which is the strangler in all but name (The Strangler Pattern)
Two weeks before cutoverThe data migration turns out to be larger than the rewrite, and the rollback path does not existThe plan covered the code and treated the data as a phase at the endEstimate and prototype the migration first, on full-size production data, including rollback (Data Migration)
Six weeks into a feature freezeThe freeze breaks for an urgent commitment; both systems now need the changeFreeze pressure — the organisation cannot actually stop for the durationDo not plan around a freeze. Assume double implementation and budget the doubled cost, or shorten the window until a freeze is unnecessary
Month five of a four-month planScope is cut from the new system; it will ship doing less than the thing it replacesThe overrun is absorbed by the only variable left, because the date and the team are fixedWrite abort and descope criteria before starting, and check them on a date, so the choice is made deliberately (Revisit Triggers)

When a rewrite is actually the right call

The conditions are nameable, and pretending they do not exist is how this lesson would lose its credibility with anyone who has shipped one. The common thread is that the *incremental path pays the same migration cost anyway* — at which point the incremental path is buying risk reduction it cannot deliver.

Note that the strongest cases have almost nothing to do with code quality. Ugly code is fixable incrementally; a data model that cannot express the business is not, and a runtime that no longer receives security patches has a clock on it that no amount of refactoring stops.

Someone proposes replacing the fulfilment service. Which situation is this?

Does the incremental path avoid the migration cost, or merely defer it?

The data model cannot express the requirement

when Multi-warehouse, multi-currency or multi-tenancy is impossible in the current schema, and every incremental step must migrate the same tables.

cost Rewrite is genuinely competitive here, because the expensive part — the migration — is unavoidable either way. Still cut over incrementally, by tenant or region (Incremental Migration).

The runtime or platform is unsupported

when The framework, language version or hosting platform receives no security patches and has no upgrade path.

cost The clock is external and does not care about your incremental plan. But scope it as a port, not a redesign — changing behaviour and platform at once doubles the unknowns.

The system is small and well understood

when A few thousand lines, behaviour fully characterized, one owner, one table.

cost Rewrite is fine and often faster. Almost none of this lesson applies at that size — say so rather than invoking cautionary tales.

The code is bad but the model is sound

when The usual case: tangled code, no tests, but the data model and boundaries broadly fit the business.

cost The loop and the strangler win comfortably. This is where rewrites are proposed most often and justified least (The Legacy Change Loop).

Nobody can state what it does

when No inventory, no tests, original authors gone, behaviour known only to production.

cost The worst case for a rewrite, and the case in which one is most often proposed. Characterize first; the inventory frequently changes the decision, and it is useful whichever way it goes.

Pricing the two paths on the same requirement

The comparison that matters is not "which produces nicer code" but what a specific requirement costs under each path, including everything that has to happen for it to reach production.

Here the requirement is one the current data model genuinely cannot express — which is the situation where the rewrite argument is at its strongest, and it is still not a walkover.

Support fulfilment from multiple warehouses per order
The change

An order's lines may be fulfilled from different warehouses, with per-line tracking and per-warehouse inventory reservation. The current schema assumes one warehouse per order, in a column on orders.

Incremental: expand-and-contract the existing schema, in place, over five releases
OrderOrderLine (new)InventoryReservationFulfilmentBatchPickListShippingLabelReturnsIntake6 reporting views
testsfulfilment_characterization_suitemulti_warehouse_testbackfill_validationdual_read_consistency_test
8 modules · 4 test files

Five releases over three months, each individually safe and revertible, with dual-write and a backfill of seven years of orders. The old assumption is embedded in six reporting views that must move in step. Delivery continues throughout.

Rewrite the fulfilment service against a model that has warehouses as first-class, cut over by region
Fulfilment (new service)Legacy OMS (frozen for fulfilment)Region router
testsbehaviour_inventory_suiteshadow_divergence_reportfull_scale_migration_rehearsal
3 modules · 3 test files

Four to six months with no fulfilment features delivered, then per-region cutover. The new model expresses the requirement directly, and the four requirements queued behind it become days rather than months.

what it cost The rewrite's cost is the six months of nothing, the doubled maintenance while both run, and the certainty that the behaviour inventory is incomplete — the escalations after the first region cuts over are not a risk, they are a line item. The incremental path's cost is that it *still* migrates the same seven years of orders, carries dual-write complexity through five releases, and leaves the business waiting three months for a capability a better model would have made trivial. Neither number is small, and choosing between them requires knowing how complete the behaviour inventory is — which is why building it is the first move under either path (Expand and Contract).

How to build it

Most important first.

  • Before anything: build the behaviour inventory from production, not from the code. Query real traffic, real data shapes, real error rates. Most "we understand it now" claims do not survive this exercise (Characterization Tests).
  • Price the incremental path honestly, including the parts that are genuinely bad — if every incremental step has to migrate the same tables, say so, because that is the strongest argument a rewrite has.
  • If you rewrite, keep the old system authoritative and shadow the new one on live traffic until divergence is boring. This is the single highest-value practice available and it is what separates rewrites that land from rewrites that become incidents.
  • Cut over incrementally even in a rewrite: by tenant, by region, by traffic percentage. A rewrite and a big-bang cutover are separable decisions, and most of the risk is in the second one (Incremental Migration).
  • Keep delivering. A rewrite that ships nothing for four months will be cancelled at month three regardless of how well it is going, because it has no evidence to offer.
  • Write down the abort criteria and the date they are checked. A rewrite without them ends by exhaustion rather than by decision.

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.

Cost of the next change
  • During the rewrite: every change costs double, because urgent fixes land in the old system and must be reproduced in the new one. Budget the doubling explicitly; teams that do not are the ones who freeze.
  • After a successful rewrite: changes are cheap, and genuinely cheaper than the strangler end-state, because there is no routing layer and no residue. This is the real prize and it should be stated, not sneered at.
  • After a failed or stalled rewrite: changes cost more than before it started, because the team now maintains two systems and the knowledge is split across both.
  • The asymmetry is the decision. The upside is a better system four months from now; the downside is a permanently worse one. Weigh those, not the elegance of the target design (The Cost of Change).
What the recommended approach costs
  • Choosing incremental over rewrite means living with the old system's constraints — its data model, its framework, its idioms — possibly for years. That is a genuine cost and the reason experienced engineers still choose rewrites.
  • The behaviour inventory, shadow traffic and incremental cutover that make a rewrite survivable add months to it, which weakens the speed argument that motivated it.
  • Refusing rewrites on principle is its own failure mode: some systems really cannot absorb the next requirement, and incrementalism there is a slow way of arriving at the same place with more spent.

What can go wrong

Failure modes
  • The second-system effect: freed from the old constraints, the new system acquires generality nobody asked for, and is late for reasons unrelated to the domain (Speculative Generality).
  • Parity is declared on the visible features and the invisible ones surface as a stream of production escalations for six months after cutover.
  • Both systems are maintained indefinitely because the last few consumers were never migrated, which is the same stalled end-state as a failed strangler but with a rewrite's upfront cost already sunk.
  • The abort point passes unnoticed, and the project continues because stopping would waste what has been spent — the sunk-cost failure, which is the most predictable one here.
  • The mitigation fails too: shadow traffic is implemented but the divergences are never triaged, so a dashboard shows 4% mismatch for four months and everyone stops looking at it.
Dependencies, and their direction
  • A rewrite depends on the old system continuing to work and continuing to be maintained, by the same people, for its whole duration. That double-staffing is the cost most estimates omit.
  • It depends on every downstream consumer, including the ones nobody has an inventory of — reports, exports, partner integrations, the analyst's spreadsheet (Do We Need a Package for This?).
  • It depends on organisational patience, which is a dependency with a shorter timeout than most engineering plans assume.
Misreads
  • "Never rewrite." Wrong, and the sort of advice that gets ignored the moment someone is holding a system that genuinely cannot go where the business needs. The claim here is narrower: a rewrite must be argued on migration and requirement discovery, not on the state of the code.
  • "The old code is bad, therefore rewrite." Code quality is the weakest of the arguments, because the loop can fix code quality incrementally and cannot fix a data model (The Legacy Change Loop).
  • "We understand the domain now, so parity is easy." The domain is not the problem; the undocumented behaviour is. Those are different bodies of knowledge and only one of them is in anyone's head.
  • "A rewrite is cleaner." Possibly, and irrelevant on its own — cleanliness is not the axis. The question is what the next requirement costs and what the transition costs, and a rewrite is a bet that the second number is small (Refactor or Rewrite).
  • "Strangler is always safer." A strangler that stalls at 40% leaves permanent dual operation, and its failure mode is quieter and therefore easier to live with indefinitely. Safer is not the same as better (The Strangler Pattern).

Testing it, and how it ages

What to test, and at which boundary
  • The behaviour inventory is the test plan. Every entry needs a test in the new system, and entries that cannot be tested are entries that will be discovered by customers.
  • Shadow comparison on live traffic, with divergences triaged by a named person weekly. An untriaged divergence log is decoration.
  • Test the data migration on a full-size copy of production data, repeatedly, including the rollback. Migrations that were only tested on a sample fail on the row that is seven years old and has a null nobody expected (Data Migration).
  • Test the abort: can you stop next Friday and leave the codebase in a coherent state? If not, that is a design problem in the plan.
How this design ages
  • A new system starts accumulating its own unknowns immediately. Whatever made the old one legacy — no tests, no owner, no written intent — will do the same here unless the loop changes (What "Legacy" Actually Means).
  • The conditions that justify a rewrite are usually temporary: an unsupported runtime gets a migration path, a data model gets a viable expand-and-contract route. Re-check the argument every few months rather than treating it as settled (Revisit Triggers).
  • Systems that were rewritten once tend to be rewritten again, because the organisation learned that rewriting is what you do. That cultural effect is real and rarely priced.

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 four failure mechanisms — undiscovered requirements, the delivery gap, data migration and freeze pressure — follow from replacing a running system rather than from any particular technology, so they recur identically across stacks and decades.
  • SCALE-SPECIFICRewriting a 3,000-line module over two weeks has none of these dynamics: there is no delivery gap worth naming, the behaviour fits in one person's head, and the data is one table. Everything in this lesson is about systems large enough that the rewrite outlives the organisation's attention span, roughly a quarter and up.
  • CONTESTEDThe strongest opposing view, stated properly: incremental migration preserves the old system's data model and boundaries, and some systems are unfixable precisely there — every incremental step still has to migrate the same tables, so the incremental path pays the migration cost repeatedly instead of once, while also carrying a routing layer and dual operation for years. Under an unsupported runtime with a security clock running, "slow and safe" can be neither. Engineers who have completed successful rewrites are not naive about the risks; they are making a different judgement about where the migration cost is concentrated. The disagreement is empirical, about the specific system, and it should be settled with a behaviour inventory rather than with quotations.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • System Design — the capacity, sequencing and traffic-shifting mechanics of running two implementations of the same service against live load.