Every monolith-to-services migration starts with the same diagram: a routing layer in front of the old system, new services growing beside it, traffic shifting route by route until the monolith can be switched off. It’s called the strangler fig, after the vine that grows around a tree until the tree is gone.
The diagram is right. It’s also the easy 20 %. I’ve been part of this kind of migration on a money-transfer platform, where the constraint was simple to state: no step may lose, duplicate or delay a transfer. Here is what the diagram leaves out.
Carve by capability, not by table
The tempting first cut is technical — “move the users table out”. It’s the wrong unit. A table is shared by a dozen code paths; moving it means changing all of them at once. Carve instead along a capability that has its own reasons to change: quotes, compliance checks, notifications, translations. A good seam is one where you can describe the inputs and outputs in a paragraph and the monolith calls it from few places.
Shadow before you shift
Before a new service takes any real traffic, run it in shadow: the router sends each request to both, the monolith’s answer is returned, and the new service’s answer is compared and logged. A week of diffs teaches you more about the old system’s undocumented behaviour — rounding, time zones, the special case for one corridor — than a month of reading its code. Only shift traffic when the diff rate is zero for reasons you understand.
Shadowing is safe for reads. For writes, it isn’t: you can’t let both systems commit the same transfer. Which leads to the real problem.
One writer at a time
The hard part of a migration is not moving code, it’s moving ownership of data. At every moment, each piece of data must have exactly one system that is allowed to write it. The usual failure is dual writes — the monolith and the new service both write, “temporarily”, and a crash between the two leaves them disagreeing in a way no one notices for weeks.
The safer sequence:
- The monolith remains the writer. Changes stream to the new service through change-data-capture or an outbox, so the new service builds its own copy and serves reads from it.
- Verify the copy continuously against the source.
- Flip ownership for a slice — one customer segment, one region — so the new service becomes the writer and streams changes back for the parts of the monolith that still read them.
- Widen the slice. Keep the flip behind a switch you can turn back in seconds.
Delete the old path
The step that never gets scheduled is deleting the monolith’s version once traffic has moved. Until it’s deleted, someone will fix a bug in it, or a forgotten batch job will still call it, and you now have two implementations drifting apart. A migration is finished when the old code is gone, not when the new code is live. Put the deletion on the plan with a date, and treat “0 calls in 30 days” as the trigger rather than a nice-to-have.
Same shape on the front end
The same ideas apply to splitting a large front end into micro-frontends: a shell that routes, slices carved by capability, one team owning each slice end to end, a shared design system as the contract between them, and the old screens deleted on a schedule. The boundary is the product; the framework underneath barely matters.
Migrations fail slowly — not with an outage but with a second system that never quite replaces the first. The antidote is unglamorous: shadow first, one writer always, and a date for the delete.