DEV Community

Nabeel Hassan
Nabeel Hassan

Posted on • Originally published at nullstud.io

Rewrites Do Not Fail in Month One. They Fail in Month Seven.

Rewrites almost never blow up in the first month. Month one feels great. The new schema is clean, the endpoints are boring in the good way, the tests are green, and everyone is happy that the horrible old thing is finally on its way out.

They blow up in month seven. The new system is roughly finished, the migration runs, and then somebody from finance opens the reconciliation report, points at a total that is off by a few thousand, and says "that number is wrong." Nobody can explain why. And because nobody can explain why, nobody is willing to be the person who says go.

I have shipped greenfield products and I have replaced systems a business was actively running on, and they are not the same job in different clothes. The second one is much harder, and the reason is almost never the technology.

A greenfield build has nothing depending on it

That is the whole difference. A new product starts from zero. Nothing is mid-transaction inside it, no customer sees its output yet, and if version one is wrong you fix it next week.

A replacement starts from a system that is already load bearing. Your team has years of muscle memory in it, your customers see its outputs, your month end close depends on it, and it encodes decisions people made years ago that nobody wrote down and everybody now relies on.

So the build is not the hard part. The excavation and the landing are, and most rewrite plans budget for neither.

The old system has a spec. It is just written in code and habit

Somewhere in that codebase is a rule like "orders from this customer group skip the credit check", or "this fee is calculated differently after the fifteenth", or "these two product codes are treated as one for reporting". Nobody remembers deciding it. It is not in any document. It exists in a conditional written by a developer who left in 2018, and in the head of an operations person who has been quietly compensating for it ever since.

Every one of those is a requirement. You just do not have the list.

This is Chesterton's fence at production scale, and the thing that makes it dangerous is that the fences are invisible until you have already torn them down. There is no annotation in the code that says "this branch is deliberate business policy" versus "this branch is a bug we adapted to". Both look identical in a diff.

The only method I trust is to go find them on purpose. Read the code paths rather than the wishlist, run both systems against the same real inputs, diff the outputs, then put the differences in front of the people who would notice, because they hold the other half of the spec.

Anyone who proposes to rebuild your system from a feature list, without excavating what the current one actually does, is quoting a different project from the one you have.

The data is worse than you were told, and nobody lied to you

The person commissioning the rewrite genuinely believes the data is fine. They have just never had to look at all of it at once.

What you will find: duplicates. Fields used for something other than their name, because in 2019 somebody needed a place to put a purchase order number and the notes column was right there. Free text where you want an enum. Rows that predate a rule change and are therefore correct by the old rules and invalid by the new ones. A long tail of entries that violate every constraint your clean new schema wants to enforce.

Here is the decision that moves the effort more than anything else: how much history has to move. All of it, the last few years, or a summary plus an archive you can query when someone asks. That is a business decision, not a technical one, and I have watched teams silently assume "all of it" and lose weeks to it.

Treat migration as its own workstream, not a step near the end. Profile what is there, agree the cleaning and mapping rules in writing, and build repeatable scripts rather than one off manual fixes, because you will run this more than once. Rehearse against production scale data at least twice: the first rehearsal exists to find out how bad the data is, the second exists to time it, because "the migration takes fourteen hours" changes a cutover plan that assumed a quiet Sunday morning.

A migration is not done when it completes without errors. It is done when someone who knows the business looks at the totals and agrees with them.

The integrations nobody calls integrations

Accounting, payments, a shipping provider, a data feed, an internal reporting tool. Those you find in the first meeting.

The ones that hurt are the ones nobody thinks of as systems. The CSV emailed every Friday and pasted into a board pack. The spreadsheet with a macro that one person maintains. The scheduled job on a machine under a desk. Every one of those is a consumer of your data contract and none of them appear on an architecture diagram. Go looking for the outputs, not just the APIs.

Big bang versus strangler fig

Two ways to land it, and this choice matters more than your stack.

Big bang. Build everything, migrate everything, switch on a date. Simpler to reason about, and you avoid running two systems at once. The problem is that all the risk arrives on one day and the fallback is a rollback nobody wants to execute in front of customers.

Incremental, one capability at a time. The strangler fig. Put the new system alongside the old one and move functions across in slices, starting with something valuable but not existential. Reporting. A customer portal. One module such as purchasing. The old system stays authoritative for everything not yet moved, and the two are kept in step until each slice is proven.

Incremental costs more in total, because you are paying for integration and synchronization that will be thrown away, and it demands real discipline about which system owns which data at any given moment. That ownership question is where most incremental migrations get messy. In exchange, no single failure takes the business down.

For anything that genuinely runs the business, I go incremental. The exception is a system that cannot be integrated with at all, and then the honest plan is a big bang with a long parallel run and a written rollback.

Parallel running is the only test suite that counts

For a period, real work goes through both systems and you compare the outputs. Shadow mode. Dual write, single read, diff everything.

Your team will not enjoy it, because it is duplicated effort against a system they are trying to kill. Do it anyway. Unlike your unit tests, it exercises the real distribution of production inputs, including the ugly ones you would never have thought to write a case for, so it is the most effective way to find the undocumented rules before they find you.

Reconcile against numbers the business already believes, not against your own expectations, which came from the same incomplete understanding that produced the bug.

Where AI actually helps, and where it does not

The build side compresses hard, and this is not a small effect. Coding agents under senior review are genuinely good at the work a replacement is full of: reading an unfamiliar codebase and explaining what a path does, drafting transformation scripts, generating the test coverage a rewrite needs, and producing the volume of ordinary screens and endpoints a business system requires. Some of the old manual processes also stop being screens at all, because a process that only existed so a human could move information between two systems can often just become automation.

What does not compress is the deciding. Which rules to keep and which were never intentional. How much history to carry. Which slice goes first. When the business is confident enough to cut over.

That matters more than it sounds, because the ratio shifts. If the build used to be seventy percent of the calendar and is now thirty, the excavation and the landing are now most of your project, and they are exactly the parts nobody staffs properly. Building faster only helps if you spend the time you saved on the parts that did not get faster.

The sequence I run

  1. Map the current system as it behaves, not as it is described. Code paths, real data, the reports people actually use, and the workarounds. The workarounds are requirements in disguise.
  2. Separate the deliberate rules from the accidents, and get that list agreed in writing.
  3. Name the first slice. Valuable, visible, survivable if it slips.
  4. Treat data as its own workstream, with profiling, rules, rehearsals and reconciliation.
  5. Run in parallel and compare until the outputs stop being a discussion.
  6. Cut over with a written rollback, then keep a team on it, because the weeks after cutover are when the last hidden rules surface.

None of that is clever engineering. It is mostly just refusing to skip the boring parts.

A replacement is not a better version of a system you already have. It is a transfer of something that is currently running, while it keeps running. Get the excavation and the landing right and the build really is the straightforward bit. Get them wrong and it does not matter how good your new system is, because nobody will be willing to switch to it.

I wrote a longer version of this for the studio site, aimed at the people commissioning these projects rather than the people building them: replacing a legacy system.

Top comments (0)