// Change at scaleWorked on 2018–2021

The strangler fig in practice: carving a service out of a monolith

Big-bang rewrites stall; carving out one capability at a time works. How to pick the first slice, route traffic at the edge, prove the new path with shadow traffic, move data ownership — and know when to stop.

#microservices#architecture#migrations#monolith

I’ve been part of breaking up monoliths more than once — checkout, refund and wallet flows at an e-commerce marketplace, and a monolithic email system at a travel company. The lesson I’d pass on from both: don’t rewrite, carve.

Big-bang rewrites tend to stall for a simple reason: the old system keeps changing while you rebuild it, so the target moves, and nothing reaches production until everything does. The strangler fig pattern, named by Martin Fowler after a vine that grows around a tree until it can stand alone, does the opposite. You put something in front of the monolith, move one capability at a time into new code, and send traffic there piece by piece. The monolith shrinks; production never stops.

Clients call a router. The router sends refund requests to a new refund service and everything else to the monolith. Over time more routes move to new services.Clients call a router. The router sends refund requests to a new refund service and everything else to the monolith. Over time more routes move to new services.
The router is the seam: each route can point at old or new code. Explore the interactive diagram ↗

Step 1: pick the first slice

The first slice decides whether the project gets a second one. Good candidates have:

  • Clear boundaries: a capability with its own inputs and outputs, like “issue a refund” or “render and send an email”, rather than something tangled through every request.
  • Real value: something that is slow to change today, scales differently, or fails differently from the rest. A slice nobody cares about won’t earn the next quarter’s funding.
  • Manageable data: ideally it owns its tables, or can.

Avoid both extremes. The easiest slice teaches you nothing; the scariest one (core checkout, usually) is a bad place to learn how your routing, deploys and monitoring behave.

Step 2: route at the edge

Put a routing layer in front of the monolith — an API gateway, a reverse proxy, or a thin facade inside the monolith that forwards certain calls. The important property: switching a route between old and new is configuration, not a deploy.

Route by whatever is natural: a URL path, a message type, a tenant, a percentage of users. Start with internal users or one low-risk segment.

Step 3: shadow, then shift

Before the new service serves real users, prove it with real traffic:

  1. Shadow: the router sends each request to both. Users get the monolith’s response; the new service’s response is compared and mismatches are logged. Only do this for reads or for writes that are safe to duplicate (or that the new service performs in a dry-run mode).
  2. Shift: move a small percentage of real traffic to the new service and watch error rates, latency and business metrics. Increase in steps.
  3. Keep the way back open: until the old code path is deleted, flipping the route back is your rollback.

Shadow comparison is where you find the undocumented behaviour — the rounding rule, the special case for one partner, the field the monolith silently fills in. Every monolith has these, and they’re why “just rewrite it” estimates are always wrong.

Step 4: move data ownership deliberately

Routing requests is the easy part. Data is where these projects stall.

The trap is a shared database: the new service reads and writes the monolith’s tables “for now”. It ships quickly, and from then on both codebases are coupled through the schema — neither can change a column without the other. You’ve built a distributed monolith.

Move ownership in stages, using the same expand–contract approach as for schema changes (see Zero-downtime migrations with expand–contract):

  1. The new service gets its own store. Data is synchronised from the monolith (change data capture, or events) while the monolith still owns writes.
  2. Writes move to the new service; the monolith receives updates through events or an API instead of writing the tables itself.
  3. The monolith stops touching those tables; they’re removed from its schema.

At each step, one system is the source of truth for each piece of data, and everyone knows which one.

Step 5: decide what “done” means

Strangler projects can go on forever. Decide early:

  • Which capabilities should move (because they change often, scale differently or need separate ownership)?
  • Which can stay in a smaller, stable monolith? A well-structured monolith for the parts that rarely change is a perfectly good outcome.
  • When will the old code paths be deleted? Every route that has fully moved should have its monolith code removed within a sprint or two. Dead paths behind flags are how a migration turns into permanent debt.

Common mistakes

Mistake What happens Instead
Starting with the hardest capability Months without a production win; project loses support Start with a valuable, bounded slice
Shared database “for now” Distributed monolith; schema changes need two teams Separate stores, synchronised, one source of truth
No shadow phase Undocumented behaviour discovered by customers Compare responses before shifting traffic
Never deleting old paths Two implementations to maintain forever Delete within a sprint of full cutover

Takeaways

  • Replace the monolith one capability at a time, never all at once.
  • Make routing switchable by configuration, so every step can be reversed.
  • Shadow real traffic before serving it; that’s where hidden behaviour shows up.
  • Treat data ownership as the real migration.
  • Define “done” — and delete the old code when you get there.
// TL;DR, in one picture
MONOLITHrefundsemailsearchwalletBIG-BANGREWRITE, 2019Don't rewrite the monolith. Grow around it.ayushwing.com