← All articles

The big-bang rewrite keeps failing. Move the system unit by unit instead.

Published

The year in the dark

A rewrite usually starts with a plan nobody argues with. Replace the old system, keep the behaviour, come back in a year with something maintainable.

What happens after that is fairly consistent. The team goes dark. The old system does not sit still while they work: bugs get fixed in it, edge cases get handled, someone asks for a report and it gets built. By the time the new system is ready, it is a faithful replacement for a version that no longer exists.

Then the new one arrives with different behaviour, and nobody can prove what changed. That is the part that does the damage. A missing feature is a ticket. Behaviour that quietly drifted somewhere inside a large codebase is a support queue with no trail back to a cause.

The reason teams pick it anyway

The big-bang is not chosen out of optimism. It is chosen because the alternative looks impossible.

A codebase that has grown for years has no clean seams. Touch one file and ten others break, because nothing in the repository records which parts really depend on which. That knowledge lives in the heads of people who may not be there anymore. Under those conditions, incremental change feels like pulling threads at random, and starting over feels like the only way out of the building.

So the honest question is not whether rewrites fail. It is whether a large, tangled system can be moved in pieces at all, with the behaviour of every piece proven before and after it moves. We wanted an answer we had actually tested, so we ran it ourselves as an R&D project rather than trying it out on a client.

Map before you move

The missing artefact is the dependency map. Not a diagram someone drew from memory, but a map derived from the code itself, showing what calls what.

Once that map exists, the system stops being a monolith and becomes an ordered list of units. The order matters more than anything else in the project: each unit moves only after the things it depends on are already in place. That sequencing is the difference between an incremental migration and a cascade of breakages.

The map also does something a plan cannot. It sizes the job before any code moves. You find out how many units there are, how tangled the worst ones are, and where the work will actually concentrate, at a point where changing your mind is still free.

Tests pin the behaviour, the API holds still

Mapping tells you the order. It does not tell you whether a unit still behaves the same after it moves. For that, each unit gets characterization tests written around its current behaviour before anyone touches it. The tests describe what the code does today, including the parts that look wrong. After the move, the same tests have to pass.

Two things follow from that. The behaviour is pinned down rather than assumed. And the migration becomes reviewable by someone who was not in the room, because the claim is not “we were careful”, it is a test suite that ran green on both sides of the change.

Alongside it, the public API is held stable while the inside changes. Downstream consumers keep working, and nobody has to schedule a dark period. You can stop between any two units and still have a working system, which is a property no big-bang rewrite has at any point before the end.

Where the agents actually fit

Most of a migration is mechanical translation, and that is the part AI agents handle well: high volume, repetitive, constrained by a defined order and a test gate that either passes or does not. The sequencing, the architectural calls, and the review stay with engineers. That division is what keeps the output reviewable instead of merely fast.

We ran this as R&D on a 44,000-line legacy application, mapped and sequenced into 14 units. It reached 11 of 14, which is 79% of the system, with 918+ tests written along the way and 576+ redundant database calls removed when the data layer was rebuilt. No API endpoint broke while it was in progress. We stopped at 79% because the method was proven by then; this was R&D, not a product we shipped.

What we did not measure: elapsed time, cost saved, or lines per day. Those figures show up in a lot of migration pitches. We did not track them on this run, so we do not quote them.

The stack underneath was Python with FastAPI, async SQLAlchemy and pytest, with graph clustering for the dependency map and Claude Code agents doing the migration work under human direction. That part is replaceable. The map, the test gate, and the stable API are not.

Full numbers and method are in the legacy migration R&D case.

Talk to us about your documents →