Skip to content

Strangler Fig Pattern: Migrating a Monolith Step by Step, with a Diagram

How to replace a monolith with the strangler fig pattern: a routing facade, four phases, what to extract first, moving data without dual writes, and proving parity.

· Founder & developer

Published

Customers call a facade API gateway. It sends /catalog/* (100 percent) to a catalog service with its own database, 25 percent of GET /orders/* to an orders service, the rest to a legacy monolith, whose database feeds the orders database through a dashed CDC path.

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/* before POST /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:

  1. 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 /orders creates a second order.
  2. 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.
  3. 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.
  4. Hold. At 100%, keep the old path deployable and its data current through events such as a month-end close.
  5. 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

Customers call a facade API gateway. It sends /catalog/* (100 percent) to a catalog service with its own database, 25 percent of GET /orders/* to an orders service, the rest to a legacy monolith, whose database feeds the orders database through a dashed CDC path.

17 components · 10 connections

Open in Archboard
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
A monolith migration in progress. The facade sends all catalog traffic to a new service that owns its data, a quarter of order reads to a new orders service fed by change data capture, and the rest to the monolith. Each step is a route change that can be undone.

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

  1. Sign in with Google or GitHub and create a diagram.
  2. Add a Person from the C4 Model pack for the customers and an API Gateway from the Cloud pack for the facade.
  3. Add three Container nodes from the C4 Model pack: Catalog service, Orders service and Legacy monolith. Color them green, amber and gray.
  4. Add a Postgres node from the Data pack to the right of each container.
  5. 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.
  6. Draw the routes from the facade, labeled with path and share, and add a Core Text status label above each container.
  7. 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.

Start free — one seat, no credit card.

Related posts

← Back to blog

Open a canvas in a minute

Sign in with Google or GitHub. Free for a single seat. Upgrade when the team joins.