Skip to content

    System Design

    Strangler Fig Migration

    Replace a legacy system piece by piece: find seams, route through a facade, ramp traffic, move data safely, bridge old and new, and finish.

    By Published 12 min read

    Share

    Standard lessonintermediateBackend System Design

    Before this: Decomposing into Microservices: Data Ownership and the Distributed Monolith

    What you'll learn: By the end of this lesson you will be able to explain the strangler fig pattern and its three phases, find a seam and choose the first slice to extract, route and ramp traffic through a facade, move a slice's data without two writers, bridge old and new with an anti-corruption layer, recognise when the pattern does not fit, and plan how a migration actually finishes.

    A restaurant cannot close for a year to rebuild its kitchen. So the owner rebuilds one station at a time: the cold station moves into a new room, orders for cold dishes are passed there, and the old station is dismantled only after a few busy weeks without trouble. Diners see the same menu throughout. The risk of each step is small, and a bad step can be undone by passing the orders back.

    That is the pattern in small. Architecture styles introduces it as a way to split a monolith, microservices decomposition says where the boundaries go, and ShopStream Stage 8 lists its steps. This lesson is the dedicated treatment: seams, the facade, the data, the bridge between old and new, and how it ends.

    Verified against Martin Fowler's article, Microsoft's Strangler Fig and Anti-corruption Layer pattern pages and AWS Prescriptive Guidance, read on 2026-11-08. The Java sample ran on Java 26.0.1 that day.

    The core idea

    The strangler fig pattern replaces a legacy system gradually: new components grow around it, traffic moves to them piece by piece, and the old parts are retired once nothing depends on them. Martin Fowler took the name from strangler figs he saw in the rainforests of Queensland in 2001: a vine germinates in a host tree, grows to the ground and can outlive the tree. He argues against the simple plan of replacing everything at once, which, he says, "we've seen ... go down in flames most of the time": users cannot wait for new features, existing behaviour is hard to specify, and much of it is unwanted anyway. He accepts that the transitional code the approach needs has a cost, and says "the reduced risk and earlier value from the gradual approach outweigh its costs."

    Three illustrations run through the lesson, each labelled:

    • Minimal example: a small Java router that sends a share of traffic to the new system, with real output.
    • Realistic example: extracting stock reservation from the ShopStream monolith.
    • Failure example: two writers for the same data during a rollback.

    The three phases

    AWS Prescriptive Guidance names the steps transform, coexist, eliminate: build modernised components alongside the legacy system; keep the monolith for rollback while an HTTP proxy at its perimeter redirects traffic to the new version; then retire the old functionality as traffic moves away. Microsoft describes four stages of the same journey: a facade is introduced and routes most requests to legacy; it shifts more to the new system each iteration; once nothing depends on the legacy system it is decommissioned; and finally the facade is removed and clients talk to the new system directly.

    A facade in front of old and new, mid-migrationClients to Facade; Facade to Legacy system (unmigrated paths); Facade to New services (migrated paths); Legacy system to New services (bridge)ClientsClientsFacade: routes by path and shareFacaderoutes by pathand shareLegacy system: shrinkingLegacy systemshrinkingNew services: growingNew servicesgrowingunmigrated pathsmigrated pathsbridge

    Figure 1. The structure of the pattern. How to read this: clients keep one address. The facade decides per request which side answers. The bridge is the temporary integration between old and new, discussed below.

    flowchart LR accTitle: One slice through transform, coexist and eliminate accDescr: A slice is chosen, built beside the legacy system, tested in shadow, ramped through the facade, then the legacy code for that slice is deleted. A rollback arrow returns traffic to legacy at any step before deletion. A["Choose a slice"] --> B["Build it beside legacy"] B --> C["Compare in shadow"] C --> D["Ramp traffic"] D --> E["Delete legacy code"] D -. "rollback: routing change" .-> B

    Figure 2. The loop repeats per slice. How to read this: each slice goes through the whole loop before the next one starts. The dotted arrow is the safety property: until deletion, rollback is a routing change.

    Finding seams and choosing the first slice

    A seam is a place where the system can be cut with little entanglement: a module with a narrow interface, a distinct set of URLs, a table only one feature uses. Fowler notes that well-designed systems would already have seams but are rare, and advises starting with small components, which carry less risk and return value sooner. Microsoft says to start after identifying new service boundaries (see domain-driven design and bounded contexts) and notes the approach lets you do "high-ROI replacements before low-ROI replacements."

    Good first slices share three traits: clear inputs and outputs, a real reason to move (scaling, a blocked team, a volatile area), and little shared state. The tangled core usually comes last.

    The pattern also has a precondition. Microsoft says it may not suit you when requests to the back end cannot be intercepted, when you cannot change the legacy system or any integration point such as its database, when the system is small enough to replace outright, or when you must decommission the original quickly.

    The facade: routing and ramping

    The facade is a proxy or gateway (AWS suggests an HTTP proxy such as Amazon API Gateway; any reverse proxy fits) that picks a side for each request. Microsoft warns that it must keep up with the migration and must not become a single point of failure or a performance bottleneck, so keep it thin: routing only, no business logic.

    Minimal example. The router below matches a path prefix, then uses a stable hash of the user id to put a fixed share of users on the new system. A stable hash matters: the same user always lands on the same side, so no one flips between two behaviours on successive clicks.

    record Rule(String pathPrefix, int percentNew) {}
    
    static Target route(String path, String userId, List<Rule> rules) {
        for (Rule rule : rules) {
            if (path.startsWith(rule.pathPrefix())) {
                int bucket = Math.floorMod(userId.hashCode(), 100);
                return bucket < rule.percentNew() ? Target.NEW : Target.LEGACY;
            }
        }
        return Target.LEGACY; // not listed: stays on the legacy system
    }

    Run with java StranglerRouter.java, with /catalogue at 100 percent, /stock at 20 percent and ten users u1 to u10. Output captured from a real run (Java 26.0.1, 2026-11-08), identical on a second run:

    /catalogue/items  NNNNNNNNNN  new=10 of 10
    /stock/reserve    LLLLLLLLLN  new=1 of 10
    /orders/42        LLLLLLLLLL  new=0 of 10
    u3 on /stock/reserve, asked twice: LEGACY, LEGACY

    Reading it: /catalogue is fully migrated, /orders still goes wholly to legacy because no rule lists it (the safe default), and /stock shows a 20 percent target landing on one of ten users. Ten users are too few to show the share; it is a hash of ten ids, not a measurement. The sample leaves out health checks, per-cohort rules and configuration reload.

    Shadowing. Before ramping real answers, many teams also send a copy of read traffic to the new system and compare its responses with legacy, discarding the new response. This is common practice, not a step either vendor page prescribes, and it applies to reads only: a shadowed write would perform the action twice. Track the mismatch count, and treat each mismatch as either a bug in the new system or an undocumented behaviour of the old one.

    Data: the hard part

    Routing is easy to reverse; data is not. Microsoft's database example moves one domain at a time out of a shared monolithic database:

    1. The new service handles the domain's requests but still reads and writes the legacy tables.
    2. A separate domain database is created. An ETL load copies history, and change data capture (CDC) keeps it in sync from the legacy database. The legacy system keeps writing to the legacy tables; the new system writes to the domain database. Consistency is validated before cutover.
    3. After validation, the domain database is the system of record. The legacy tables, procedures and sync processes for that domain are removed.

    Rollback is possible through phase 2 and at the start of phase 3, while the tables and sync still exist. After they are removed it "significantly increases effort and risk", so Microsoft says to treat removal as a deliberate final step per domain.

    Moving one domain's data: copy, sync, then cut overNew service to Legacy database (step 1: reads, writes); Legacy database to ETL load, then CDC (step 2: copy, sync); ETL load, then CDC to Domain DB; New service to Domain DB (step 3: after validation)New serviceNew serviceLegacy database: record at firstLegacydatabaserecord at firstETL load, then CDCETL load, thenCDCDomain DB: record after cutoverDomain DBrecord aftercutoverstep 1: reads, writesstep 2: copy, syncstep 3: after validation

    Figure 3. A database strangler. How to read this: step 1 uses the legacy tables, step 2 adds a synchronised, validated copy, step 3 switches the new service to the copy. The legacy side is cleaned only after validation.

    A design rule, ours rather than Microsoft's, follows from the failure example below: for any given row, exactly one side is the writer at any moment. Reads can be duplicated across both sides; writes cannot.

    Coexistence: the anti-corruption layer

    While both systems run, the new one may call unmigrated legacy functions, and the legacy one may call migrated ones. Microsoft recommends an anti-corruption layer (ACL), an adapter that translates between the two models so the new design is not bent into the legacy one's shape. It describes the costs plainly: added latency, one more service to run and scale, and a decision about whether the layer is permanent or retired after migration. It also advises keeping the layer to translation only, with no business rules or orchestration, and validating input at the boundary.

    Where legacy code cannot be changed, Microsoft notes the database can feed new services: a trigger that sends messages, or CDC reading the transaction log.

    When it does not fit

    flowchart TD accTitle: Should this system be strangled accDescr: Ask whether requests can be intercepted, whether the legacy system can be changed or has an integration point, whether the system is small, and whether the original must be decommissioned quickly. Only when interception is possible, change is possible, the system is large and there is time does the pattern fit. Q1{"Can requests be intercepted?"} -->|"no"| N1["Not suitable"] Q1 -->|"yes"| Q2{"Can you change legacy code or an integration point?"} Q2 -->|"no"| N1 Q2 -->|"yes"| Q3{"Small enough to replace in one go?"} Q3 -->|"yes"| N2["Replace directly"] Q3 -->|"no"| Q4{"Must the original go very soon?"} Q4 -->|"yes"| N1 Q4 -->|"no"| Y["Strangler fig fits"]

    Figure 4. The preconditions from Microsoft's guidance as a decision. How to read this: any "no" at the interception or change questions rules the pattern out; a small system or a short deadline does too.

    Concern Strangler fig Big-bang rewrite
    Risk per step Small, reversible until deletion Concentrated at cut-over
    Value delivered Early, slice by slice At the end
    Cost Transitional facade, bridge, two systems running One system, but feature work usually stalls
    Rollback A routing change Often not possible
    Main failure Never finishing Never finishing, or finishing wrong

    The other side of this topic

    On the front end, the same idea migrates pages or components to a new framework or to micro-frontends behind one shell. That is a planned lesson in the Frontend System Design track, not yet published, so there is no link.

    In practice: extracting stock reservation from the ShopStream monolith

    This is the realistic example on ShopStream, an educational reference design. No new numbers are used beyond the ramp percentages of Stage 8; the scenario is illustrative.

    This is a realistic scenario, not a case history.

    The situation

    Checkout in the monolith reserves stock with a conditional update inside the same database transaction that creates the order. Inventory changes now ship on a different cadence from orders, and a store feed import slows checkout. The team wants Inventory as its own service, following the order in Stage 8.

    Starting architecture

    Before: checkout and stock share one transactionAPI gateway to Monolith; Monolith to PostgreSQL (one transaction)API gatewayAPI gatewayMonolith: checkout + stockMonolithcheckout + stockPostgreSQL: orders and stockPostgreSQLorders and stockone transaction

    Figure 5. The starting point. How to read this: order creation and stock decrement commit together. Splitting them gives up that single transaction, which is the real cost of this extraction.

    Options considered

    Option Gain Cost Risk
    A. Rewrite checkout and inventory together One design Months without releases Fowler's "go down in flames" case
    B. Extract reads first, then writes Reversible steps CDC feed, two phases Slow
    C. Extract writes first Fastest to the goal Distributed reservation at once Overselling during the switch

    The decision

    Option B, because it follows the data steps above. Phase one: the new Inventory service serves availability reads from a copy kept in sync by CDC; the monolith remains the only writer. Phase two: the monolith's checkout calls a reservation endpoint on the new service through an ACL, with an idempotency key, and writes move one group of stores at a time. The old tables go last.

    Rollout and verification

    Shadow the reads and compare answers, then ramp 1, 10, 50 and 100 percent at the facade, as in Stage 8, watching error rate, p99 latency and mismatch count. Before moving a store group's writes, compare per-SKU quantities between the two databases and require zero differences. Keep the legacy tables until the group has run a full release cycle.

    The 2 a.m. failure

    The page reads: oversold units on three stores. A deploy rolled the facade back for one store group, so checkout wrote stock to the monolith again while the new service had already accepted reservations for that group. Two writers, two truths. Figure 6 shows the sequence.

    sequenceDiagram accTitle: Two writers after a routing rollback accDescr: The facade sends a store group's reservations to the new service. A rollback sends the next reservation to the monolith, which still holds an older copy of stock. Both acknowledge the last unit. participant S as Shopper participant F as Facade participant N as New service participant M as Monolith S->>F: reserve last unit (group routed to new) F->>N: reserve N-->>S: reserved, stock 0 in new database Note over F: deploy rolls the routing back S->>F: another shopper reserves the same unit F->>M: reserve M-->>S: reserved, stock still 1 in the old copy

    Figure 6. A routing rollback is not a data rollback. How to read this: the router moved back, the data did not. Both sides acknowledged the same last unit.

    The first check is a per-SKU diff between the databases. The mitigation is to freeze reservations for the group, treat the new database as the truth for post-cutover reservations, and apologise or substitute for the oversold units. The lasting fix is to enforce the single-writer rule in the data layer: after cutover the monolith's stock write path rejects that group, so a routing rollback fails loudly instead of writing.

    Architect's notes

    • Make rollback a routing change until the moment of deletion, and rehearse it.
    • One writer per piece of data at all times, enforced in code, not only in the router.
    • Keep the facade thin and deploy it independently.
    • Count the transitional cost: facade, bridge, CDC, two systems.
    • Set an end date per slice, with deletion in the definition of done.
    • Fowler's last advice: change the organisation too, or the new system "will end up in a similar mess".

    Common mistakes

    • Leaving the old code in place after the traffic has moved, so two implementations survive.
    • Moving reads and writes at once.
    • Putting business rules in the ACL or the facade.
    • Starting with the most tangled core.
    • Treating the facade as free: it can be a single point of failure.
    • Removing legacy tables before the validation window ends.

    Key takeaways

    • Strangler fig replaces a system slice by slice behind a facade, in three phases: transform, coexist, eliminate.
    • It needs interceptable requests and a legacy system you can change or integrate with.
    • Start with small, well-bounded slices; the tangled core comes last.
    • Ramp by stable hash so users do not flip between systems; rollback is a routing change.
    • Data is the hard part: copy, sync with CDC, validate, then cut over; one writer at a time.
    • An ACL protects the new design; keep it translation-only and decide whether it is retired.
    • A migration is finished only when the old code is deleted.

    Practice exercises

    Exercise 1 (30 min). Extend the router with a header that pins an internal tester to the new system and a rule that overrides the hash. State the order of precedence.

    Exercise 2 (45 min). List five seams in a system you know and rank them by Fowler's and Microsoft's criteria for a first slice.

    Exercise 3 (1 hour). Write the validation query that compares per-SKU stock between the legacy and domain databases and the pass condition for cutover.

    Exercise 4 (1 hour). Design the ACL calls between the monolith's checkout and the new reservation endpoint. What does it translate, and what must it not do?

    Exercise 5 (2 hours). Plan the retirement: the checklist for deleting the facade, the bridge and the legacy tables for one slice.

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments