Every modernisation programme I have joined arrived with a diagram showing the monolith on the left, tidy services on the right, and an arrow in the middle. The arrow is where the entire budget goes.
The part everyone gets right
Carving out the first service is genuinely straightforward. Pick a bounded capability with few inbound dependencies, put a facade in front of the monolith, route a fraction of traffic to the new implementation, compare, ramp up. The pattern works, it is well documented, and teams execute it competently.
Then they try to do it a second time, and discover the problem was never the code.
The part that breaks
The monolith's real asset is a single transactional database where every invariant is enforced for free. orders, inventory and payments in one commit means nobody ever had to think about what happens between them.
The moment you extract inventory, that free correctness becomes your problem. Suddenly you own:
- Two systems that must agree on stock, neither of which is authoritative during migration
- Reports written against joins that no longer exist
- A reconciliation job somebody has to be woken up for
- An eventual-consistency window the business has never been asked to accept
That last one is the real blocker, and it is not technical. Somebody in operations has to agree that for up to nine seconds, stock may be wrong. If nobody will sign that, the migration stops — no matter how good the engineering is.
What worked for us
Three things, in this order.
Change data capture before service extraction. Before moving any logic, we streamed the monolith's writes out to the new stores. For months, the new services were read-only shadows: same data, no traffic, continuously comparable. When we finally routed writes, we were not hoping the model was right — we had a season of evidence.
A reconciliation report nobody could ignore. Not a dashboard. A daily figure, in a channel the operations director read, showing rows where old and new disagreed. It started at thousands. Making that number visible did more for data quality than any amount of design review.
-- Ran hourly through the whole migration. The most valuable
-- forty lines of SQL in the programme.
SELECT m.sku,
m.qty_on_hand AS monolith_qty,
s.qty_on_hand AS service_qty,
m.qty_on_hand - s.qty_on_hand AS drift
FROM monolith.inventory m
JOIN inventory_svc.stock s USING (sku)
WHERE m.qty_on_hand <> s.qty_on_hand
AND m.updated_at < now() - interval '60 seconds' -- past the window
ORDER BY abs(m.qty_on_hand - s.qty_on_hand) DESC;The interval '60 seconds' clause is the whole trick: it distinguishes genuine divergence from data that is merely in flight. Without it the report cries wolf and gets ignored within a week.
Deleting the old path on a scheduled date. A strangler that never strangles is just a second system. We set the decommission date at the start, in writing, with a named owner. Two of the migrations slipped. All of them finished — which puts us well ahead of the industry.
What I would do differently
I would spend the first month mapping reporting dependencies rather than transactional ones. Every migration I have run was delayed by finance, not by engineering: some critical monthly figure turned out to depend on a join across three tables that were about to live in different databases, and nobody knew until close.
The monolith you are replacing is not the application. It is the set of assumptions the whole company has quietly built on top of it.
Find those assumptions first. The code is the easy half.
Filed under
- Migration
- Legacy
- Data
- Cloud