Modernization
Rewrite the seam, not the system
ZenHorizon Engineering · 8 October 2024 · 5 min
Someone always proposes a rewrite in October. The monolith is “unmaintainable”. The vendor is sunsetting a module. A new CTO wants a clean graph on the whiteboard. We have been in that room enough times to be suspicious of the word clean.
A rewrite is a product freeze wearing a technical costume. While the new system is “almost ready”, the old one still takes the traffic, still gets the emergency patches, and still holds the only honest data. Eighteen months later you have two systems and a migration plan that slipped twice.
We are not religious about old code. We have deleted plenty of it. We are religious about not stopping the business so a new repo can feel pure.
Find the seam that is already on fire
We do not start with modules. We start with the incident log and the support queue. Where do people wait? Where does a human retype data from one screen into another? Where does a batch job fail on the first of the month and nobody notices until accounts call?
That seam is the rewrite. Not “the backend”. A seam has a contract: inputs, outputs, an owner, and a way to run old and new side by side. If you cannot run both, you do not have a migration. You have a cutover prayer.
A useful tell: if two teams argue about who owns a table, you have found a seam, or you have found a missing product. Either way, do not start by redrawing services around the org chart. Start by writing the contract the rest of the company already believes exists.
A pattern that has worked more than once
- Put a facade in front of the old path. Same URL or same queue name. Callers do not get a science project.
- Write the new path for one tenant, one region, or one product line. Production, not a demo database.
- Shadow traffic for a week. Compare outputs. The mismatches are the real spec. They are never in Confluence.
- Flip a flag. Keep the old path for 30 days. Delete it only after the on-call week is boring.
Shadowing is the step teams skip because it feels like delay. It is the opposite. A week of compared outputs will surface the three business rules that lived in a stored procedure and a fourth that lived in a support agent’s head. You wanted those before you cut over. You were not going to get them from a workshop.
The data is the product
Teams love to redraw services and forget that the old database is a museum of decisions. Nulls that mean three different things. A status column with values nobody will explain on the record. Dates stored as strings because a contractor in 2017 was in a hurry.
We budget time to sit with the person who has been closing the month for five years. They will tell you which fields are sacred and which screens are theatre. That conversation is architecture.
If you need a rule: do not migrate data you have not queried in 90 days into the new core. Park it. Prove someone needs it. Warehouses are cheap. Wrong constraints in the new schema are not.
Dual-write is fashionable and dangerous. If you write to old and new, you now have a distributed transaction you did not want. We use it only when the seam is a write the business cannot pause, and we put a reconciler next to it from day one — a job that lists the disagreements. Disagreements are the work. Hiding them in two happy databases is how you schedule a weekend.
When a rewrite is actually justified
Sometimes the platform is gone. A vendor will turn off an API. The runtime is out of support and the risk team has a date. Fine. Even then, rewrite the business capability, not the org chart of the old codebase.
- If the old system is a .NET Framework app on a single VM, the seam might be “quotes” or “settlement”, not “the API layer”.
- If you are leaving a monolith, extract the write path that hurts first. Reads can sit on a replica longer than your pride wants.
- If you are leaving a low-code tool, export the data and the workflow, not the screens. Screens are the easy part.
- If the language is the problem — a runtime nobody will hire for — wrap the old process and replace the edges that hireable people can own.
A full stop-the-world rewrite is still the right call in a few cases: the old system cannot accept a facade, the data is small enough to move in a maintenance window you can actually get, and the product is simple enough that you are not secretly redesigning it. That intersection is rarer than keynotes suggest.
Staff the old system on purpose
The political failure is always the same. The A-team goes to the new repo. The old system gets a rotating cast and a Slack channel that feels like a waiting room. Then a Sev-1 happens on the old path, because that is where the traffic is, and leadership asks why modernization is “creating risk”.
Keep an owner on the old path until the flag is gone. Pay them like the work matters. If you cannot staff both, you cannot afford the rewrite you sketched. Shrink the seam until you can.
Flags, not dates
Cutover dates slip. Flags do not have to. We ship the new path dark, turn it on for one tenant, then for a percentage, then for everyone who is not on a denylist. The denylist is where the weird data lives. That is a feature. Those tenants teach you the last rules.
Measure the seam with the same request id on both paths. If the new path is slower, you will know before the CFO does. If it disagrees on an amount, you stop the rollout. Pride is not a rollback plan.
Document the flag in the same one-pager as the seam. Who can flip it, what “done” means, and which tenant is the canary. If that lives only in a Slack thread, the next on-call will treat the flag like a rumour.
We will still stand up a new repo. We will still pick a boring stack. We will not pretend the old system vanished because the slide said Phase 2.