🧭 Analogy
A strangler fig seeds in the branches of a host tree, sends roots down around the trunk, and gradually envelops it — the new plant grows while the old still supports it, until eventually the original is gone. Migrating a monolith works the same way: the new system grows around the old, both coexist, and you replace the host one branch at a time.
The pattern
Martin Fowler’s metaphor (2004) gives us a three-step, incremental migration that supports pause, stop, and rollback at every point:
sequenceDiagram participant C as Client participant P as Proxy / interceptor participant N as New microservice participant M as Monolith C->>P: Request alt functionality migrated & redirected P->>N: Route to new service N-->>C: Response else not yet migrated P->>M: Pass through to monolith M-->>C: Response end
- Identify the parts to migrate.
- Migrate the functionality to a new microservice (copy or reimplement — copy, don’t delete).
- Redirect calls from the monolith to the new service.
Deployment is not release
A crucial discipline: until calls are redirected, the new functionality is not live even when deployed. So you can stand up the service early — returning 501 Not Implemented — deploy it to production, set up monitoring, and “sit with it a while” to get comfortable operating it. Then redirect later. Each step is easily reversible: switch the redirection back for instant rollback.
A worked example — the HTTP reverse proxy
- Insert the proxy first as a pure pass-through (assess latency, set up monitoring before changing anything).
- Migrate functionality — stand up the new service returning
501, deploy to production (deployed, not released). - Redirect calls only once all the functionality is moved — use a feature toggle, and consider a canary or parallel run at the proxy.
graph LR subgraph P1["1. Insert proxy (pass-through)"] C1["Client"] --> Px1["Proxy"] --> M1["Monolith"] end subgraph P2["2. Deploy new service (501, not released)"] Px2["Proxy"] --> M2["Monolith"] Px2 -. "deployed only" .- N2["New service (501)"] end subgraph P3["3. Redirect when ready"] Px3["Proxy"] --> N3["New service"] end
Proxy advice: prefer a dedicated proxy like NGINX (the author’s two hand-coded proxies were “incredibly inefficient”; extend NGINX in Lua for custom behaviour). Redirecting by URI/REST resource is easy; switching on a request-body parameter is harder.
💡 Keep the pipes dumb
A proxy can transform protocols (SOAP→gRPC), but logic piling up there makes a shared proxy a contention point. “Keep the pipes dumb, the endpoints smart.” Prefer pushing mapping into the service — support old and new protocols internally. Square migrated to gRPC via a service mesh (a local proxy per instance, central control plane, built on Envoy) for exactly this reason.
Where it fits — and where it doesn’t
The strangler fig moves functionality without changing the existing system, so it shines when the monolith is worked on by others or is a black box. It works cleanly for end-to-end vertical slices with no downstream dependencies, and “shallow” extractions that still call back into the monolith are possible. It is often the first port of call — HTTP is very amenable; FTP and even batch-file uploads can be strangled (Homegate intercepted FTP uploads by watching the server log).
Requirement: you must be able to map an inbound call to the asset you’re moving. When functionality is buried deep inside with no external call to redirect, the strangler fig fails — use branch by abstraction instead.
⚠️ Don't change behaviour mid-migration
Rollback assumes the monolith and the microservice are functionally equivalent. If the new service quietly adds a bug fix or a feature, rolling back reintroduces the bug or removes the feature — and there’s no easy fix. Prefer a feature freeze on the migrating part and keep each migration small so there’s no pressure to “slip a feature in.”
🔑 Key insight
The strangler fig is the canonical first migration pattern because it is light-touch, works on black boxes, and is reversible at every step. Its only hard requirement is an interceptable inbound call.
See also
- Modeling service boundaries — deciding what to strangle first.
- Migration patterns — branch by abstraction, parallel run, and friends.
- Decomposing the database — the harder half of any extraction.
When to use it — and when not
✅ Reach for it when
- You are incrementally extracting functionality from a monolith you can route around.
- The functionality has an inbound call you can intercept at the perimeter.
- You want a low-risk, reversible migration — even for black-box systems.
⛔ Think twice when
- The functionality is deep inside the monolith with no external call to redirect — use branch by abstraction.
- You plan a big-bang rewrite (don't).
Related topics
Where to draw the lines — using information hiding, coupling/cohesion, the four (or five) types of coupling, and just-enough domain-driven design (aggregates and bounded contexts) to find stable boundaries.
ms-decompositionMigration Patterns: Branch by Abstraction & Parallel RunBeyond the strangler fig — branch by abstraction for functionality deep inside the monolith, parallel run to verify a risky replacement, plus decorating collaborator and change data capture.
ms-decompositionDecomposing the DatabaseMicroservices must own their data, so migration means splitting the shared database — coping patterns, ownership transfer, and the hard losses of joins, foreign keys, and ACID transactions.
Check your understanding
Score: 0 / 41. What are the three steps of the strangler fig pattern?
Identify what to move, migrate (copy or reimplement) it to the new microservice, then redirect inbound calls from the monolith to the service.
2. Why can you deploy the new service before redirecting any traffic?
Until calls are redirected the functionality isn't 'live' even when deployed, so you can get comfortable operating it and redirect later — each step easily reversible.
3. When does the strangler fig pattern work poorly?
Strangler fig requires mapping an inbound call to the thing you're moving; for deep-inside functionality use branch by abstraction instead.
4. Why copy rather than delete code when extracting?
Remove the original only once migration is confirmed; until then the monolith copy is your safety net.
Comments
Sign in with GitHub to join the discussion.