CMD Guide
HomeSystem DesignMicroservices Patterns

The Architecture of the Strangler Pattern

The Strangler Fig pattern works by inserting a thin interception layer — a facade, proxy, or gateway — directly in the request path in front of the legacy system, so that individual capabilities can be re-routed to new services one at a time while every un-migrated request silently falls through to the old code, letting both systems run in parallel and the new one grow until the old is dead.

The name comes from the strangler fig, which germinates in the canopy of a host tree, sends roots down around its trunk, and eventually replaces it — the host is never felled, it is gradually superseded. Martin Fowler borrowed it to describe migrating off a monolith without the risk of a big-bang rewrite. The single most important structural fact: the facade is the seam. All traffic enters through it, so the decision of "old or new?" is made in exactly one place, per capability, and can be changed without redeploying either backend.

The facade is a router, not a gatekeeper toll-booth

Calling the facade a "gatekeeper" undersells it. Its real job is to hold a routing table keyed by capability (usually an HTTP route or an RPC method) and, for each request, answer one question: does this capability, for this specific caller, belong to the new service yet? Because the answer is data, not code, you can dial a capability from 0% to 100% of traffic live, and dial it back to 0% just as fast when the new service misbehaves. That reversibility is the whole point.

diagram
diagram

A worked migration: the orders capability

Say a monolith owns everything and we want to peel out order handling into a new service. We split the capability into orders.read and orders.write because reads are safe to canary first (worst case: a stale list) while writes carry the real risk (a lost or duplicated order). The facade routes by a stable hash of the user ID into a bucket 0..99, then compares that bucket to the rollout percentage. Same user, same bucket, every time — so a user does not flip back and forth between systems on consecutive clicks.

Here are real bucket values (SHA-1 of "orders.read:<uid>", first 8 hex digits, mod 100) and where each user lands as we ramp the read rollout:

User IDbucketread @ 1%read @ 25%read @ 100%
812323LEGACYNEWNEW
5534LEGACYLEGACYNEW
4055174LEGACYLEGACYNEW
9021088LEGACYLEGACYNEW

Read this vertically: as the percentage rises, a user only ever moves left-to-right, legacy→new, and never back. A user with bucket 23 joins the canary the moment rollout crosses 23%; bucket 88 waits until nearly the end. Dropping the percentage back to 0% (a rollback) cleanly returns everyone to legacy. The ramp we actually shipped:

Dayorders.readorders.writeWhat we are watching
01%0%error rate + p99 latency on the new read path
325%0%data parity: new reads == legacy reads?
7100%5%write correctness on a tiny slice
14100%100%legacy write path now dark
21delete the orders code from the monolith; remove the routes from the facade

The router, done right

The original toy predicate — "use the new service for even user IDs" — looks fine and is quietly broken. This version is what actually ships:

import hashlib

class StranglerFacade:
    """Front door for the monolith. Routes each capability to the new
    service by rollout percentage; everything else falls through
    to the legacy system unchanged."""

    def __init__(self, legacy, new, rollout):
        self.legacy = legacy      # client for the monolith
        self.new = new            # client for the new service
        self.rollout = rollout    # {"orders.read": 100, "orders.write": 5}

    def _bucket(self, capability, routing_key):
        # Stable 0..99 bucket: same key -> same bucket, forever.
        digest = hashlib.sha1(f"{capability}:{routing_key}".encode()).hexdigest()
        return int(digest[:8], 16) % 100

    def _in_canary(self, capability, routing_key):
        pct = self.rollout.get(capability, 0)
        if pct <= 0:
            return False              # capability still fully legacy
        if pct >= 100:
            return True               # fully migrated
        return self._bucket(capability, routing_key) < pct

    def handle(self, capability, routing_key, request):
        if self._in_canary(capability, routing_key):
            return self.new.dispatch(capability, request)
        return self.legacy.dispatch(capability, request)   # fall through

Why the naive version is wrong. userID % 2 == 0 hard-codes a fixed 50% split you cannot ramp — there is no 1% canary and no way to reach 100%; it is 50-or-nothing. It gives you no rollback lever (the code, not a config value, decides), and because the parity of an ID is arbitrary, a user's reads and writes can land on different systems with no coherent story about which holds their truth. The hash-bucket version fixes all three: any percentage is expressible, the decision is a config value you can flip live, and it is sticky and monotonic — raising the percentage only ever moves users from legacy to new, never the reverse, so nobody's session thrashes mid-migration.

The hard part the routing hides: data consistency

Routing a request is easy. The genuinely difficult problem is that one order record now has two possible writers. While orders.write is at 5%, some orders are created by the new service and some by the monolith, and every reader must see all of them. You have three real options, in rough order of increasing safety and cost:

Whichever you pick, the migration order must respect it: migrate reads before writes so a read-side bug is harmless, and never let a capability's writes go to the new store while a large fraction of its reads still hit the old one unless a sync keeps them in step.

Pitfalls

When to use it — and when not to

Reach for the Strangler Fig when: the system is large and business-critical (you cannot afford a freeze or a risky cutover), it stays in active use during the migration, and its functionality decomposes into capabilities you can peel off one at a time behind a network boundary (HTTP routes, RPC methods). The signal is "we cannot stop the world, and the rewrite will take quarters not weeks."

Trade-offs versus the alternatives:

Choose strangler when incremental, reversible, capability-by-capability migration of a live distributed system is worth the price of an extra hop, a routing layer to operate, and a period of dual-running with a consistency story. Prefer a rewrite when the system is small, and branch-by-abstraction when the swap is in-process.

Takeaways


Sources: Martin Fowler, "StranglerFigApplication" (martinfowler.com); Sam Newman, Monolith to Microservices (O'Reilly, 2019) — chapters on the Strangler Fig, Branch by Abstraction, and Parallel Run; Microsoft Azure Architecture Center, "Strangler Fig pattern." Router code and traced bucket values authored and executed for this guide. Re-authored / Deepened for this guide.

🤖 Don't fully get this? Learn it with Claude

Stuck on The Architecture of the Strangler Pattern? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **The Architecture of the Strangler Pattern** (System Design) and want to truly understand it. Explain The Architecture of the Strangler Pattern from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **The Architecture of the Strangler Pattern** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **The Architecture of the Strangler Pattern** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **The Architecture of the Strangler Pattern** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes