I’ve written a monolith-to-microservices playbook before that mostly stayed at the level of architecture decisions — bounded contexts, service boundaries, the stuff of diagrams. The part that actually determines whether a migration succeeds is less glamorous: how do you move traffic away from the old system without a rewrite that takes eighteen months and ships nothing usable until the very end. The strangler fig pattern is the answer that’s actually been proven at scale, and it’s worth understanding as a routing problem first, an architecture problem second.
The Name Is the Whole Idea
Martin Fowler borrowed the term from a real plant: a strangler fig germinates in the canopy of a host tree, sends roots down to the ground, and gradually grows around the host until the original tree dies and rots away — leaving the fig standing in exactly its shape, having never existed as a single organism that had to be built and then swapped in one motion. Applied to software: new functionality gets built as a separate service and traffic gets incrementally redirected to it, route by route, until the legacy system handles nothing and can be switched off. At no point does anything “cut over” — the system that exists at 3pm on a Tuesday is a hybrid of old and new, and that’s the plan working correctly, not a sign it’s half-finished.
The Piece That Actually Makes This Work: a Routing Facade
Everything hinges on inserting something between clients and the backend that can send different requests to different places, invisibly to the caller:
server {
listen 443 ssl;
server_name api.example.com;
location /api/v1/users {
proxy_pass http://new-user-service:8080;
}
location /api/v1/ {
proxy_pass http://legacy-monolith:9000;
}
}Nginx path-based routing is the simplest version of this; an API gateway (Kong, AWS API Gateway) or a service mesh (Istio, Linkerd) does the same job with more control — canary percentages, header-based routing for internal testing, automatic rollback on error-rate spikes. The specific tool matters far less than the discipline of routing by route, not by service: /api/v1/users goes to the new service, everything else still goes to the monolith, and that boundary moves one endpoint at a time as each piece gets rebuilt and verified.
Order of Extraction: Start With What’s Actually Safe to Get Wrong Once
The instinct is often to extract whatever’s most painful first — the piece everyone’s afraid to touch. That’s backwards for a first attempt: the first extraction should be low-risk and well-understood, because its real purpose is proving the pattern works in your specific environment (the routing facade, the deployment pipeline for the new service, the rollback procedure) before betting anything important on it. A good first candidate: a read-heavy, low-write endpoint with no complex transactional coupling to the rest of the monolith — a product catalog lookup, a user-profile read, something where “briefly serving slightly stale data during cutover” is a non-event rather than a data integrity problem.
The Trap: Reimplementing Business Logic Instead of Reusing It
The most common way strangler migrations quietly fail is subtler than a bad extraction order: a team extracts a service and reimplements its business rules from scratch, from documentation or tribal knowledge, instead of from the monolith’s actual code — and the new service almost imperceptibly diverges. A pricing calculation that’s 99% the same as the original is worse than one that’s obviously different, because it fails silently in the 1% of cases nobody tested, and nobody’s watching for it because the migration “already shipped.” The safer path, even though it feels like extra work: extract the actual logic first — as a shared library, or literally by having the new service call back into the monolith for that one calculation during a transition period — and only rewrite it once the extraction itself is proven stable and the two implementations have run in parallel long enough to confirm they agree.
Data Is Harder Than Routes
Routing traffic is the easy 80%. The genuinely hard part shows up the moment an extracted service needs its own database instead of querying the monolith’s shared schema — because now two systems can disagree about the same entity’s state, and something has to reconcile that. Two real patterns, not mutually exclusive:
- Dual writes during the transition: the monolith keeps writing to its own table and to the new service’s store (via an API call or a message), so both stay in sync while some traffic still hits the old path. Fragile if not handled carefully — a write that succeeds in one place and fails in the other needs an explicit reconciliation strategy, not silence.
- Change Data Capture (Debezium reading the monolith’s database WAL, publishing changes to a queue): lower coupling than dual writes, since the monolith’s code doesn’t need to know the new service exists at all, but it introduces replication lag as a real variable the new service has to tolerate.
Whichever approach, the actual finish line for a given piece of data isn’t “the new service can read it” — it’s “the monolith’s copy has been deleted and nothing still depends on it,” and that step gets skipped more often than it should, leaving a permanent dual-write system nobody intended to keep.
How You Know It’s Actually Done
Not when the new services handle more traffic than the monolith — that milestone is easy to hit and easy to declare victory at while the monolith still holds critical, rarely-touched functionality nobody wants to migrate. The real finish line: the monolith’s deployment pipeline can be deleted, its database has no remaining writers outside of the new services, and turning its servers off on a Tuesday afternoon is a non-event. Everything short of that is progress, not completion — and it’s worth saying so explicitly in project status updates, because “80% of traffic migrated” and “the legacy system is 80% off my plate” are very different claims that get conflated constantly.
The Checklist
- Put a routing facade in place before extracting anything — it’s the mechanism, not an implementation detail to add later.
- Extract something low-risk first to prove the pattern in your environment, not the piece that hurts the most.
- Reuse the monolith’s actual business logic during transition rather than reimplementing it from memory or docs.
- Plan the data migration path (dual writes or CDC) explicitly — “the new service can read the data” is not the same milestone as “the old copy is safe to delete.”
- Call it done when the legacy system’s shutdown is a non-event, not when traffic percentage crosses some round number.
