Migrating off a monolith with the strangler fig

How to move off a legacy monolith without a rewrite: the facade, what order to move modules in, a wave planner, and the reasons migrations stall.

Every legacy system reaches the meeting where someone says the sensible thing: this codebase is fighting us, let us rewrite it properly. It is sensible and it is usually wrong. Joel Spolsky called rewriting from scratch "the single worst strategic mistake that any software company can make" in 2000, writing about Netscape, whose rewrite left them years without a competitive browser.

The alternative has a name and a picture. Martin Fowler watched strangler figs in Queensland on holiday in 2001: a seed germinates in a branch of a host tree, grows down around it, and eventually stands on its own while the original tree dies inside it. His StranglerFigApplication applies that to software, and his argument against the big replacement is practical rather than aesthetic: wholesale replacements "seem easy to specify, but often it's hard to figure out the details of existing behavior", users cannot wait, and much of the old behaviour "isn't really wanted, so building it is a waste".

When I joined Acefone the company was already mid-migration from a PHP monolith to MERN microservices. I took the greenfield side and defined the service patterns while the migration was still underway, which meant the conventions for new services were set before there were five of them rather than after. That timing mattered more than any individual technical decision we made, and it is the part most migration plans leave out: the order you do things in is the plan.

This post is about that order. How the pattern works, what to move first, a planner that turns your module list into waves with a timeline, and the specific reasons these projects stall.

How the pattern actually works

A facade sits in front of the legacy system and decides, per request, which side answers. At the start it routes everything to the monolith. Each wave moves one capability behind that facade, so callers never learn that anything changed. Microsoft's write-up of the strangler fig pattern sets out the same four phases: introduce the facade, shift requests incrementally, decommission the legacy system when nothing depends on it, and finally remove the facade itself.

A diagram showing clients calling a facade, which routes some requests to new services and the rest to the monolith, with both reading shared data. Below it, four phases: put a facade in front, move one capability and release it, repeat until both systems run side by side, then remove the monolith and the facade.
The facade makes the migration invisible to callers. The shared data underneath it is where the real work is.

Two warnings from that same page are worth taking literally. The facade must not "become a single point of failure or a performance bottleneck", because you have just put a new component on the path of every request, and everything on that path multiplies into your uptime. And where the new system needs to call unmigrated functionality, or the reverse, an anti-corruption layer keeps the old system's assumptions from leaking into the new one.

The part nobody draws is the data. While a capability is half-moved, two systems read and write the same records. You will need a synchronisation path, usually change data capture from the old database to the new one, a reconciliation job that proves the two agree, and a decision about which side is the system of record for each table at each moment. Any retry in that machinery means work can happen twice, which is why idempotency stops being theoretical the week you start.

What to move first

The instinct is to start with the worst module, because it hurts the most. That is the wrong first move: it is usually the most coupled, so you will spend the first three months building seams while shipping nothing, and the team will not yet know how the pattern behaves in your system.

Order by three properties:

  • Coupling is how many other parts reach into it. High coupling means the module cannot move until the things around it have clean interfaces, so it goes late.
  • Risk is what happens when it misbehaves: money moved twice, a customer locked out, a regulator interested.
  • Size is the work to move it, in person-weeks. Small modules early give you a finished wave to point at.

The first wave should be something small, independent and visible, so the team learns the facade, the deployment, the dual-running and the rollback on a module where being wrong is cheap. Notifications and search are the usual candidates: they are leaf nodes, they are easy to verify, and nobody's month-end depends on them.

The last wave is whatever everything else depends on. In most business systems that is auth, billing or the core transaction table, and often all three.

Plan your own waves

Put your modules in with an honest size in person-weeks, and rate risk and coupling from 1 to 5. The planner orders them, packs them into waves at your team's capacity, and holds anything with a coupling of 4 or more until the end.

planner · waves, timeline and what goes last

Order a strangler fig migration

the modules in the monolith · size in person-weeks, risk and coupling from 1 to 5

WAVES
TIMELINE
TOTAL WORK
LEAVE FOR LASTcoupling of 4 or more

the plan, wave by wave

Modules are ordered by coupling first, then risk, then size, so the cheap and independent pieces go early where the team is still learning the pattern, and anything with a coupling of 4 or more is held back until the seams around it exist. Waves are filled in that order without reshuffling, which is why some waves are half empty: a module larger than what remains of a wave waits for the next one. Size is person-weeks of work, not calendar weeks.

The defaults describe a nine-module system with 47 person-weeks of work and four engineers on the migration. With four-week waves and a fifth of the team's capacity going on running both systems, each wave holds 12.8 person-weeks, which gives six waves: 24 weeks of waves plus three weeks to build the facade first, so about 27 weeks, or a little over six months.

A wave plan chart. Three weeks to build the facade, then six four-week waves. Wave one carries notifications, search and reporting at 10 person-weeks. Wave two carries the admin panel and webhooks. Wave three carries customer profiles. Waves four, five and six carry billing, auth and orders individually, marked as the modules everything depends on.
The first waves carry three modules each. The last three carry one apiece, because the things everything depends on are also the biggest.

Two things in that plan are worth reading carefully.

The waves are lumpy. Only 61% of the available capacity is used, because a module that does not fit in what remains of a wave waits for the next one. That is not a flaw in the arithmetic; it is what happens when large modules meet fixed-length waves. The fix is not a cleverer packer, it is finding a seam inside the big modules: splitting orders into "read the order" and "change the order" turns one ten-week wave into two smaller ones that each ship sooner.

The last three waves each carry one module: billing, auth, then orders. If the project is going to be cancelled or starved, it will be starved around wave four, when the easy wins are done and the remaining work is all load-bearing. Knowing that in advance is the argument for doing the facade and the data synchronisation properly at the start, because those are what make wave four survivable.

What the plan does not show

The dual-running tax. Two deployment pipelines, two sets of alerts, two on-call surfaces, and a reconciliation job somebody has to read. The planner takes 20% of capacity for this by default and that is not pessimistic.

Contract tests on the events. My own lesson from Acefone is to put the event schema under contract tests on day one. We got the interaction model right and the versioning wrong, and paid for it twice during rollout. Schema discipline is cheap before the first consumer and expensive after the fifth.

Backlogs during cutover. The moment you move a write path, queues behave differently, and a migration is exactly when someone discovers there is no backpressure or that a backlog can explode while both systems are catching up.

The organisation. Fowler's article points at Conway's Law: legacy systems are brittle partly because of the structures that produced them. If the same team boundaries survive the migration unchanged, the new system tends to grow the same seams as the old one.

Where migrations stall

Feature parity as the goal. The patterns of legacy displacement written by Ian Cartwright, Rob Horn and James Lewis identify this directly: a desire for feature parity was a key factor in several failures they saw, because agreeing what the current system even does becomes a huge effort and pushes teams towards one big cutover. Much of what the old system does is unused. Find out before you rebuild it.

No decommission date. If nothing forces the monolith off, it stays, and you now run two systems permanently at twice the operational cost. Put the shutdown in the plan with a date, and treat a slipped date as a decision rather than a drift.

The facade becomes the new monolith. Routing rules accumulate business logic, and three years later the thing you cannot change is the router. Keep it dumb: match a route, pick a side, forward.

Stopping at ninety per cent. The last modules are the hardest and the least visible, and by then the pressure to return to feature work is enormous. This is the state most "we did microservices" stories are actually in, and it is the worst of both: the complexity of two systems and the benefits of neither. Starting a migration is also worth weighing against not starting one, because splitting a system too early creates the same overhead without the legacy problem to justify it.

quick check

You are moving a PHP monolith to services. Which module should go in the first wave?

The first wave is where the team learns the facade, the dual running and the rollback. Do that on a leaf module where a mistake is cheap. Auth and billing are the most coupled, so they move last, once the seams around them exist.

Rewrite or strangle

A rewriteA strangler fig migration
Value arrives at the end, if it arrivesValue arrives every wave
Needs a full specification of current behaviourNeeds only the next capability understood
One enormous release to get rightMany small releases, each reversible
Feature work freezes for the durationFeature work continues on both sides
Failure means nothing shippedFailure means you stop with part of it done and keep that part
Cheaper on paper: one system at a timeGenuinely more expensive while both run

The last row is the honest cost of the pattern, and Fowler concedes it: a transitional architecture is code you will throw away, and it looks wasteful, but "the reduced risk and earlier value from the gradual approach outweigh its costs". You are buying optionality with that waste. At any point you can stop, and what you have already moved keeps working.

questions people ask

What is the strangler fig pattern?

A way of replacing a legacy system gradually. You put a facade in front of it, move one capability at a time to a new system behind that facade, and decommission the old system once nothing depends on it. Named by Martin Fowler after strangler figs, which grow around a host tree until it is gone.

How long does a monolith migration take?

It depends on the work and the capacity you can actually protect. The worked example here, nine modules and 47 person-weeks with four engineers on four-week waves, comes to about 27 weeks including three weeks to build the facade. Expect a fifth of the team's capacity to go on running both systems.

What should you migrate first in a strangler fig migration?

Something small, loosely coupled and low risk, such as notifications, search or reporting. The first wave exists to teach the team the facade, the dual running and the rollback on a module where being wrong is cheap.

What goes last?

Whatever everything else depends on, usually authentication, billing and the core transaction path. They move once the modules around them have clean interfaces, because otherwise you build seams for months without shipping.

Is it ever right to rewrite instead?

Occasionally: a very small system, a platform that is genuinely going away, or a product whose behaviour you are deliberately changing wholesale. The test is whether you can specify the current behaviour cheaply and whether users can wait. For most businesses, neither is true.

What is the most common reason these migrations fail?

Aiming at feature parity and having no decommission date. The first makes the project enormous, the second makes it endless, and together they produce a company running two systems permanently.

The short version

Do not rewrite. Put a facade in front of the legacy system, move one capability at a time, and release each one so that value arrives every few weeks rather than at the end of a project that may never end.

Order the moves by coupling first: cheap, independent modules early while the team is learning, and the things everything depends on last, once the seams around them exist. Budget for the tax of running two systems, put contract tests on the events before the second consumer exists, and write the monolith's shutdown date into the plan on the first day. The migrations that fail are rarely beaten by the technology. They stall at ninety per cent, with the easy modules moved, the hard ones untouched, and nobody willing to name a date for switching the old thing off.

S

Sanjeev Sharma

Product Engineer at Acefone, building real-time communications at carrier scale: WhatsApp, voice and IVR in one agent inbox. Built and runs PostEngage, a WhatsApp automation SaaS, on his own. Contributor to litellm and the Vercel AI SDK. Takes on a small number of consulting engagements each year.