Your monolith is ten years old, every release needs a long regression cycle, and someone proposes a rewrite: build the new system alongside, reach feature parity, switch over on a weekend. Meanwhile the old system keeps changing and the switch-over date slips. Martin Fowler writes that he and his colleagues have seen this plan "go down in flames most of the time".
The strangler fig pattern replaces the system piece by piece instead. A facade in front of the monolith routes each request to the old code or a new service, and capabilities move one at a time until nothing routes to the monolith. Routing is the easy part. Data ownership, proving parity and finishing are the hard ones.
This post covers all of them, with a diagram of a migration in progress.
Where the name comes from
In 2001, Fowler saw strangler figs in the Queensland rain forest: vines that germinate on a host tree, root in the soil and can outlive the tree, leaving the fig in its shape. In June 2004 he used them as a metaphor for replacing a legacy system gradually. He renamed the post from "Strangler Application" to "Strangler Fig Application" in April 2019, to point back to the plant, and rewrote it in August 2024. The current version stresses what teams resist: transitional architecture, code you build only so old and new can coexist.
The facade does the routing
The facade is a reverse proxy or API gateway in front of the monolith: NGINX, Envoy, a cloud API gateway or a load balancer with path rules. It starts by sending everything to the monolith. After that, each migration step is a routing change. Route by:
- Path:
/catalog/*goes to the catalog service. - Method:
GET /orders/*beforePOST /orders, so reads move first. - Share: a hash of a session or user ID keeps each user on one side.
- Header or tenant: internal users or a pilot customer go first.
# upstream blocks catalog, orders and monolith defined elsewhere
split_clients "${cookie_session}" $orders_read {
25% orders; # canary share for reads
* monolith;
}
map $request_method $orders_target {
GET $orders_read;
default monolith; # order writes stay in the monolith
}
server {
location /catalog/ { proxy_pass http://catalog; }
location /orders/ { proxy_pass http://$orders_target; }
location / { proxy_pass http://monolith; }
}
Rollback is one edit: point the 25% line at monolith (nginx rejects a 0% entry). Hash a key every request carries: requests without the cookie all land in one bucket.
Keep the facade thin: routing, TLS, authentication and logging, no business rules. It sits on every request, so run several instances and measure the latency it adds. The API gateway architecture diagram covers what belongs in a gateway.
The four phases of a migration
| Phase | The facade routes | Data | Done when |
|---|---|---|---|
| 0. Facade first | Everything to the monolith | Unchanged | Facade live, added latency measured |
| 1. First capability | One route, such as /catalog/*, to a new service |
Service reads monolith tables or a CDC copy, then its own | Route at 100% through a full business cycle |
| 2. Most traffic on new services | Most routes; the monolith keeps a known list | Each service owns its tables | What is left fits on one page |
| 3. Monolith retired | Nothing to the monolith | Legacy database archived, then deleted | Monolith switched off |
Microsoft's Strangler Fig pattern page adds a final step: remove the facade, or keep it as an adapter for legacy clients.
Choosing what to extract first
Good first candidates have three traits:
- A clear seam, in Michael Feathers' words a place where you can alter behavior without editing in that place: a URL prefix, a queue, a module boundary.
- A high change rate, so the move pays off soon. Version control shows it.
- Low coupling: few joins into other tables, few calls back into the monolith.
git log --since="12 months ago" --format= --name-only -- src/ \
| sort | uniq -c | sort -rn | head -20
A ranking for an online store:
| Candidate | Seam | Change rate | Coupling | Verdict |
|---|---|---|---|---|
| Catalog browsing | /catalog/* |
High | Low | First |
| Order history | GET /orders/* |
Medium | Medium | Second, reads first |
| Checkout | Calls pricing, stock and payments | High | High | After its dependencies |
| Nightly reports | SQL across the schema | Low | Very high | Last, or a warehouse feed |
Extract by business capability, not by technical layer. A shared "data access service" moves nothing and couples everything.
Moving the data
Routing moves requests, not data, and data is where migrations stall. Decide who owns each table at each step.
| Strategy | How it works | Use it for | Risk |
|---|---|---|---|
| Shared database | The new service uses the monolith's tables | The first weeks | Monolith schema changes break it |
| Change data capture | A connector streams row changes from the monolith's log | Moving reads first | Lag, legacy schemas, one-way flow |
| Database per service | The service owns its tables; others call its API | The end state | Joins across services become API calls |
| Dual writes from code | The application writes to both databases | Nothing | A crash between writes leaves them out of sync |
Microsoft's guidance gives the usual order: the new service starts on the monolith's tables, then gets its own database, loaded once and kept current by CDC, and becomes the system of record after validation. Until the legacy tables are removed, rollback is a route change; after that, a restore.
Avoid dual writes: a crash between two separate writes leaves the databases disagreeing, and concurrent updates can land in different orders. Make one transaction the source of every change, through CDC or a transactional outbox. The change data capture diagram covers Debezium and replication slots.
The anti-corruption layer
The monolith's model leaks: one-letter status codes, billing and shipping fields in one table, rules in stored procedures. A service that consumes those rows inherits the model you are leaving. An anti-corruption layer, a pattern from Eric Evans' Domain-Driven Design (2003), translates at the boundary:
- Legacy to new: CDC events carrying legacy rows become orders in the new model.
- New to legacy: the orders service reads customer data from the monolith through an adapter.
- Monolith to new: once the catalog has moved, monolith code calls the catalog API instead of its old tables.
Keep business rules out of the layer. In a hexagonal service it lives in the adapters, so the core never sees legacy types.
Proving parity before a route moves
Every route goes through the same steps:
- Shadow. The facade copies requests to the new service and discards its responses, as NGINX's mirror module and Envoy's request mirroring do. Shadow reads only: a mirrored
POST /orderscreates a second order. - Compare. Diff both responses after normalizing timestamps, IDs and ordering. Twitter's Diffy, open-sourced in 2015, runs two copies of the old code and ignores what differs between them as noise. Inside the monolith, GitHub's Scientist library (Ruby, with ports to other languages) runs old and new paths side by side and returns the old result.
- Canary. Move 1%, 5%, 25%, 50%, then 100% of real traffic, compare error rate and p99 latency with the monolith's for the same route, and hold each step through a daily peak. The deployment strategies diagram covers the mechanics.
- Hold. At 100%, keep the old path deployable and its data current through events such as a month-end close.
- Delete the old code, its tables and the route.
Flipping a route back only works while the monolith has current data. Once the new service owns writes, feed them back by reverse CDC or events, or a rollback loses them.
How strangler fig migrations fail
- Never finishing. The easy 80% moves; reports, batch jobs and admin screens stay, and you run two systems. Count what is left every sprint and set a switch-off date.
- A distributed monolith. Services that share tables, deploy together or call each other in long synchronous chains. The test is one writer per table.
- The facade as a bottleneck: business logic in the gateway, or one instance carrying all traffic.
- Hidden consumers. Reports and other teams read the monolith's database directly, unseen by the facade. Check connection logs before moving a table.
- Feature parity as the goal. Fowler notes that much legacy behavior "isn't really wanted". Patterns of Legacy Displacement covers this and other traps.
The strangler fig migration diagram
Diagram contents
- Customers
- Facade (API gateway)
- Catalog service
- Orders service
- Legacy monolith
- Catalog DB
- Orders DB
- Monolith DB
- Debezium + Kafka
- Anti-corruption layer
- HTTPS
- /catalog/* · 100%
- GET /orders/* · 25%
- everything else + 75% of GET /orders
- reads, writes
- reads
- WAL changes
- order row events
- upserts in the new model
How to read it:
- Left to right: customers call the facade, which splits traffic by route; databases sit to the right.
- Green, moved: all of
/catalog/*goes to the catalog service, which owns its data. - Amber, canary: 25% of
GET /orders/*goes to the orders service. Writes and the other reads still go to the monolith. - Gray, legacy: the monolith serves the rest and still owns order data.
- Purple, dashed: Debezium streams order changes from the monolith's WAL through Kafka, and the anti-corruption layer maps them to the new model.
- The route table lists target, share and phase per route, so rollback is one edit.
Drawing strangler fig migrations clearly
- One diagram per phase, or a dated title; keep the old ones to show the direction.
- Label routes with path and share, such as "GET /orders/* · 25%".
- Show data ownership: the system of record and the direction of every sync.
- Color by status (moved, canary, legacy), not by team.
- Leave out internals. This is a migration map, not a component diagram.
- Draw hidden consumers, such as a nightly report reading the monolith's database.
The microservices architecture diagram shows how to keep the end state readable.
Build it in Archboard
- Sign in with Google or GitHub and create a diagram.
- Add a Person from the C4 Model pack for the customers and an API Gateway from the Cloud pack for the facade.
- Add three Container nodes from the C4 Model pack: Catalog service, Orders service and Legacy monolith. Color them green, amber and gray.
- Add a Postgres node from the Data pack to the right of each container.
- Between the monolith's database and the orders database, stack a Kafka node from the Data pack (Debezium + Kafka) and a Component from the C4 Model pack (Anti-corruption layer), connected upward with dashed edges.
- Draw the routes from the facade, labeled with path and share, and add a Core Text status label above each container.
- Add a Markdown block from the Core pack with the route table.
If your route map lives in a Mermaid flowchart, the Mermaid converter turns it into an editable first draft.
The example uses 11 diagram nodes, within the Free plan.
FAQ
What is the strangler fig pattern? A way to replace a legacy system gradually: a facade routes each request to the old code or a new service, capabilities move one at a time, and the old system is switched off when nothing routes to it.
Why is it called the strangler fig pattern? Martin Fowler named it in 2004 after strangler figs he saw in Queensland, which grow around a host tree and can replace it. He added Fig to the name in 2019 to stress the plant.
How do you handle the database in a strangler fig migration? In steps. The new service first reads the monolith's tables, then gets its own database fed by change data capture, and takes over writes after validation. Avoid dual writes from application code.
What is the difference between strangler fig and branch by abstraction? The strangler fig routes requests at the system's edge. Branch by abstraction works inside the code: an interface lets old and new implementations coexist until every caller has switched. Many migrations use both.
When should you not use the strangler fig pattern? When requests to the old system cannot be intercepted, when the system is small enough to replace at once, or when it must be switched off quickly.