Module 3: The Strangler Fig Pattern

Building the new service alongside

Overview

In the previous lesson you put the facade in front of the legacy and verified it's transparent: the control point got installed, at 0%, without changing a single response. Now comes the second phase of the strangler fig, and it's where you finally write new code: building the new service alongside the legacy. The key word is alongside. You don't replace anything, you don't touch the legacy, you don't cut any wire. You raise a new implementation —the modern_catalog— that lives in parallel, that responds to the same GET /products, and that the facade will be able to call when you decide.

Here there's a distinction that's the heart of the lesson: the new service has to respect the same public contract as the legacy —the same endpoint, the same response shape, the same keys— but it's free to have a completely different internal implementation. The legacy maybe stores the products in a flat list; the modern can store them in a store with stock, in another database, in another language even. What matters is that, seen from outside —from the facade and from the client—, the two speak the same language. It's that equality of contract that lets the facade swap them without the client noticing the change.

And this is what makes the strangler so safe: the new service is built without any pressure to break the old one, because the old one keeps serving 100% of the traffic while you develop. You can take your time, write the modern well, test it calmly, run it against the characterization tests of module 2 —and only when it's ready does the facade start sending it a trickle of real traffic—. Building alongside is building in peace.

Connection with the module. Lesson 2 put the facade (phase 1); this one builds the new service alongside (phase 2). With the two pieces in place —the facade that decides and the modern that already exists— lesson 4 does phase 3: divert the traffic by percentage, raising the traffic_percent so the facade starts calling the modern you build here. Notice the boundary: here we build the modern as a function/service that coexists and we verify it meets the same contract, but we do not yet do the exhaustive old-vs-new comparison that detects discrepancies before trusting —that's the parallel-run of module 6, which compares the responses of the two over real data and reports the differences—. Here we only confirm that the new one responds with the same contract; validating it thoroughly with historical data is the data migration's job. And how the modern is built inside —whether it's a microservice, event-oriented, with what database— is a decision of the style and data guides; here we care only that it exists alongside and speaks the contract.

An analogy: the new kitchen built without closing the old one

Let's return to the previous lesson's restaurant. You already have the waiter (the facade) taking all the orders and taking them to the old kitchen. Now you want a new kitchen —with better ovens, faster, better laid out—. Do you build it by closing the restaurant for two months? No: you build it in the space next door, while the old kitchen keeps serving the diners every day.

While you build it, the restaurant doesn't lose a single service. The diners eat just like always, served by the old kitchen. You, in peace, set up the new kitchen: you install the ovens, hire the team, tune the recipes. And here's the critical point: the new kitchen has to produce the same menu dishes —a hamburger has to come out being a recognizable hamburger, with its bun, its patty, its ingredients—, even though inside the new kitchen works completely differently: other ovens, another workflow, another way of organizing the ingredients. The diner orders "hamburger" and receives a hamburger; they don't care —nor notice— which kitchen it was made in.

The menu is the public contract: GET /products, with its response shape. The kitchen is the internal implementation: the legacy's flat list, or the modern's store with stock. Building the new kitchen alongside, with the same menu but a different kitchen inside, and without closing the old one, is exactly what this phase of the strangler does. When the new kitchen is tested, the waiter will start sending it some orders —few at first—, and there lesson 4 comes in.

Worked example: the modern alongside the legacy, same interface, different kitchen

We're going to build the modern_catalog alongside the legacy_catalog and demonstrate three things: that they have different internal implementations, that they respond with the same public contract, and that the facade can divert a 20% canary to the new one while the old one keeps serving the rest. Notice the difference in structure: the legacy uses a flat list of products; the modern uses a store —a dictionary with stock per product—. They're different kitchens, and even so the GET /products returns the same.

import zlib

# === LEGACY: old implementation. Flat list, no changes. Not touched. ===
LEGACY_ROWS = ["ssd", "usb-hub", "webcam", "hdmi"]
def legacy_catalog(request):
    q = request.get("q", "")
    items = [p for p in LEGACY_ROWS if q in p] if q else list(LEGACY_ROWS)
    return {"status": 200, "source": "legacy", "products": items}

# === MODERN: new service, built ALONGSIDE. Different internal structure, ===
# === same public contract. Developed and tested without touching the legacy. ===
MODERN_STORE = {
    "ssd":     {"name": "ssd",     "stock": 12},
    "usb-hub": {"name": "usb-hub", "stock": 3},
    "webcam":  {"name": "webcam",  "stock": 0},
    "hdmi":    {"name": "hdmi",    "stock": 20},
}
def modern_catalog(request):
    q = request.get("q", "")
    names = [row["name"] for row in MODERN_STORE.values() if q in row["name"]] if q \
            else [row["name"] for row in MODERN_STORE.values()]
    return {"status": 200, "source": "modern", "products": names}

# === The facade can point to either of the two. Here, a 20% canary. ===
def bucket(request_id):
    return zlib.crc32(str(request_id).encode()) % 100

def strangler_router(request, traffic_percent):
    if bucket(request["id"]) < traffic_percent:
        return modern_catalog(request)
    return legacy_catalog(request)

# --- Coexistence: both alive. We verify that modern meets the same contract. ---
def satisfies_contract(resp):
    return (resp.get("status") == 200
            and isinstance(resp.get("products"), list))

sample = [{"id": i, "path": "GET /products", "q": ""} for i in range(1, 11)]

print("New service built ALONGSIDE the legacy - 20% canary\n")
print(f"{'req':>4}{'route':>10}{'products':>28}{'contract ok?':>14}")
print("-" * 56)
for req in sample:
    resp = strangler_router(req, traffic_percent=20)
    ok = satisfies_contract(resp)
    print(f"{req['id']:>4}{resp['source']:>10}{','.join(resp['products']):>28}{('YES' if ok else 'NO'):>14}")

# The legacy stays intact; the new one already responds with the same public contract.
print("-" * 56)
print("\nPublic contract identical (same 'products' key, same GET /products).")
print("INTERNAL structure different: the legacy uses a list; modern, a store with stock.")
print("The legacy wasn't touched: the new one lives beside it and the facade chooses which to call.")

What to expect. When you run the file, the output is exactly this:

New service built ALONGSIDE the legacy - 20% canary

 req     route                    products  contract ok?
--------------------------------------------------------
   1    legacy     ssd,usb-hub,webcam,hdmi           YES
   2    legacy     ssd,usb-hub,webcam,hdmi           YES
   3    modern     ssd,usb-hub,webcam,hdmi           YES
   4    modern     ssd,usb-hub,webcam,hdmi           YES
   5    legacy     ssd,usb-hub,webcam,hdmi           YES
   6    legacy     ssd,usb-hub,webcam,hdmi           YES
   7    legacy     ssd,usb-hub,webcam,hdmi           YES
   8    legacy     ssd,usb-hub,webcam,hdmi           YES
   9    modern     ssd,usb-hub,webcam,hdmi           YES
  10    legacy     ssd,usb-hub,webcam,hdmi           YES
--------------------------------------------------------

Public contract identical (same 'products' key, same GET /products).
INTERNAL structure different: the legacy uses a list; modern, a store with stock.
The legacy wasn't touched: the new one lives beside it and the facade chooses which to call.

Read the route column: of the 10 requests, three (numbers 3, 4, and 9) went to the modern and seven to the legacy. That's the 20% canary in action —the stable bucket sends those three ids to the new route—. But the interesting thing isn't just that some went to the new one, but that in the products column the response is identical whether it comes from the legacy or the modern: ssd,usb-hub,webcam,hdmi in all ten rows. The client that receives any of these responses can't know which kitchen produced it. That's the same menu from two kitchens.

And the contract ok? column gives YES in all ten rows. The satisfies_contract function verifies the essential of the contract: that the response has status == 200 and that products is a list. That the modern passes that verification on each request is what gives the facade permission to send it traffic: if the modern returned a different status, or a products that isn't a list, breaking the contract would break the client, and the facade shouldn't call it. The contract verification is the minimum the new service must meet to coexist.

Notice the detail of the different kitchens. The legacy iterates over LEGACY_ROWS, a list of strings. The modern iterates over MODERN_STORE.values(), dictionaries with name and stock, and extracts only the name to build the response. They're two implementations that don't share a single line of internal logic —the modern even has information the legacy doesn't have, the stock— and even so they produce the same public output. That independence is deliberate: the modern is a reimplementation from the contract, not a copy-paste of the legacy. It can evolve on its own (tomorrow the stock will be used to hide sold-out products), but today, to coexist, it only needs to speak the same GET /products.

Deep dive: the contract as a boundary, and what "alongside" means

The public contract is the boundary between what the outside world sees and what each service does inside. As long as the two services respect that boundary, the facade can swap them freely. It's the same idea that sustains all programming against interfaces: the caller depends on the what (the contract), not the how (the implementation). The strangler takes it to system scale: the GET /products is the interface, and legacy_catalog and modern_catalog are two implementations of it that the facade chooses by percentage.

                    GET /products  (the contract: status 200, products: [...])
                          │
          ┌───────────────┴───────────────┐
          │                                │
   legacy_catalog                    modern_catalog
   (flat list)                       (store with stock)
   the old kitchen                   the new kitchen, alongside

"Alongside" has a precise technical meaning: the modern doesn't share mutable state with the legacy in a way that one could break the other. They're independent in their execution. In this module we simulate them as two functions in the same process —enough to see the traffic diversion—, but the idea scales: in production, the modern is usually a separate process or service, with its own deployment. What doesn't change with the scale is the essential property: building the new one doesn't require touching the old one. The legacy serves 100% of the traffic while the modern is built and tested; only when the modern is ready does the facade start giving it work.

There's a temptation worth naming: wanting the modern to be "better" from day one —more features, better format, faster—. In this phase, the modern's goal isn't to be better, it's to meet the contract. A modern that does exactly what the legacy does, but with a clean and tested implementation, is already a huge advance: it's a slice of the monolith rewritten under control. The improvements come later, when the modern already serves the traffic and you can evolve it safely. First contract parity; then, improvements. (And careful: the exhaustive comparison of whether the modern really produces the same as the legacy over all the data —not just the shape, but the values— is the parallel-run of module 6. Here we verify the shape of the contract; there the equivalence of content is verified.)

Common mistakes

Copying the legacy instead of reimplementing from the contract. What happens: to "go fast," the team copies the legacy's code to the modern and makes cosmetic adjustments. Why it happens: reimplementing from scratch is scary and copying seems safe. How to spot it: the modern carries the same quirks, the same bugs, and the same tangled structure of the legacy —including the misunderstood tacit knowledge of module 1—. How to fix it: the modern must be built from the contract and from the characterization tests (module 2), not from the old code. The characterization tests tell you what behavior to preserve (including the bugs that do matter); from there, you write a clean implementation that passes those tests. Copying the legacy leaves you with two copies of the same problem; reimplementing from the contract leaves you with a clean and understood version. The point of the strangler isn't to move the legacy, it's to replace it with something better built.

Breaking the contract "to improve once and for all." What happens: the team takes advantage of the modern to change the response shape —rename products to items, add a wrapper, change a field's type—. Why it happens: the modern is new code and it gives the sense that "now we can do it right." How to spot it: satisfies_contract fails, or —worse— it passes the lax verification but the real client breaks because it expected products and received items. How to fix it: in the coexistence phase, the contract is sacred: the modern responds with exactly the same shape as the legacy, because the facade swaps them without warning the client. Changing the contract is a change that all the clients must coordinate, and you can't do it with a silent canary. If you want to evolve the contract, that's an API versioning —topic of the API design guide— and it goes after the modern has already replaced the legacy, not during the migration.

Building the modern by touching the legacy "just a little." What happens: when building the modern, the team modifies the legacy to "share" a function, extract a common utility, or "clean up along the way." Why it happens: duplicated code is seen between the two and the instinct is not to repeat. How to spot it: the legacy has new commits during the modern's construction phase —when it should be frozen and intact—. How to fix it: in this phase the legacy is not touched. "Alongside" means independence: the modern can duplicate logic of the legacy if needed, because that duplication is temporary —the legacy is going to be retired—. Sharing code between the old and the new couples them right when you want to separate them, and a change in the shared utility can break the legacy that serves 100% of the traffic. Temporary duplication is the correct price of keeping the legacy frozen and safe.

Exercises

Exercise 1 — Same menu, different kitchen. In the example, the legacy uses a flat list and the modern a store with stock, but both return products: ["ssd","usb-hub","webcam","hdmi"]. (a) Why is it a good sign that the internal implementations are different? (b) The modern has information the legacy doesn't have (stock). Should it expose it in the GET /products during the coexistence phase? (c) When could it start using the stock?

See solution

(a) Because it demonstrates that the modern is a genuine reimplementation from the contract, not a copy of the legacy. If the modern had the same internal structure as the legacy, it would probably be a copy-paste that carries its problems. Different structures that produce the same public output prove that the modern understands the contract (the what) and solves it with its own kitchen (the how) —which is exactly the point of the strangler: replace, don't move—.

(b) No, during coexistence not. The facade swaps legacy and modern without warning the client; if the modern returned a stock field the legacy doesn't return, the responses of the two routes would be different, and a client that receives stock sometimes and not others would have erratic behavior depending on the bucket. In the coexistence phase, the modern responds with exactly the same contract as the legacy, even if internally it knows more.

(c) The stock can start being exposed after the modern has replaced the legacy at 100% and the legacy is retired —when there are no longer two routes that must match—. At that moment, adding stock is an evolution of the contract (a versioned API change, topic of the API design guide), not a discrepancy between routes. First the modern catches up to and replaces the legacy with contract parity; then, on its own, it evolves.

Exercise 2 — The contract verification. The example's satisfies_contract function only checks status == 200 and that products is a list. (a) Is that verification enough to trust that the modern produces the same as the legacy? (b) What kind of problem would it not detect? (c) In which module is the verification done that does detect that problem?

See solution

(a) It's not enough to trust total equivalence. satisfies_contract verifies the shape of the response (that it has status 200 and a list of products), which is the minimum for the client not to break structurally. But it doesn't verify that the values are correct.

(b) It wouldn't detect that the modern returns the wrong list of products but with the correct shape. For example, if for q="usb" the legacy returns ["usb-hub"] and the modern returns ["usb-hub","usb-cable"] (because its filtering has a bug), both responses pass satisfies_contract —both are lists with status 200— but the modern is giving a different result. The shape verification doesn't see the content differences.

(c) The verification that does detect content differences is the parallel-run of module 6: run the two implementations over the same real requests, compare their responses field by field, and report the discrepancies before trusting the modern. In this phase (module 3) we verify that the modern can coexist (it meets the shape of the contract); in module 6 we verify that the modern is equivalent to the legacy over real data. They're two different levels of confidence, and both are needed.

Exercise 3 — Alongside, really. A team builds the modern and, to avoid duplicating code, extracts a parse_query() function from the legacy into a shared module both import. (a) What "alongside" principle does this violate? (b) What concrete risk does it introduce over the legacy, which serves 100% of the traffic? (c) Why would duplicating that function be, here, the safest option?

See solution

(a) It violates the principle that building the modern must not touch the legacy. Extracting parse_query() into a shared module modifies the legacy (now it imports from elsewhere instead of having its own function) and couples it to the modern right when the goal is to separate them. "Alongside" means independent, not "sharing guts."

(b) The risk is that a change in the shared function —made for the modern— breaks the legacy by mistake. The legacy serves 100% of the traffic; if tomorrow someone adjusts parse_query() for a case of the modern and that adjustment changes the parsing's behavior, the legacy —which also imports it— starts failing for all the users. You coupled the system you want to retire with the one you're building, and you added a path by which the new can break the old.

(c) Because the duplication here is temporary: the legacy is going to be retired. Duplicating parse_query() in the modern leaves the legacy exactly as it was —frozen, safe, with no new dependencies— and gives the modern its own copy that can evolve without affecting anyone. The general rule "don't duplicate code" yields to the strangler's rule "don't touch the legacy you're migrating": the duplication disappears on its own when the legacy is retired, and meanwhile it buys independence. It's the correct price of keeping the old one intact.

Summary and next step

In this lesson you did the second phase of the strangler fig: building the new service alongside. You saw, with the new kitchen raised in the space next door without closing the old one, that the modern is built in peace while the legacy serves 100% of the traffic. And you executed it: you set up a modern_catalog with a different internal implementation —a store with stock, not the legacy's flat list— but the same public contract, and you verified, with a 20% canary, that the two routes return identical responses and that the modern meets the contract on each request. The legacy stayed intact; the new one lives beside it; the facade chooses which to call.

Before moving on you should be able to: distinguish the public contract (which must be identical) from the internal implementation (which can be different); explain why the modern is built from the contract and the characterization tests, not by copying the legacy; argue why the legacy isn't touched when building the modern, even if it means duplicating code; and know what a shape contract verification verifies —and what it does not verify— versus the parallel-run of module 6.

Lesson 4 does the third phase and the heart of the module: diverting the traffic by percentage. Now that the facade is in place and the modern exists alongside, you're going to raise the traffic_percent from 0 to 100 —0→10→50→100— and see, measured over 1000 requests, the traffic move from the old to the new. And you're going to add the piece that makes the diversion safe: the fallback, the old route as a safety net when the modern throws an error. You're going to discover something revealing: even at 100% of the traffic "on the new," the case the modern doesn't yet handle keeps falling to the legacy —and that tells you, with numbers, that you can't retire it yet—.

Resources

  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), ch. 3 — the construction of the new service in parallel to the monolith and the role of the shared contract that lets them be swapped. The central reference of this phase. In English.
  • Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. The new system that grows alongside the old until it replaces it: the image of the fig applied to building in parallel. In English.
  • Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004) — why reimplementing from the characterization tests (not by copying the legacy) preserves the behavior that matters without carrying the old structure. The bridge with module 2. In English.
  • Chris Richardson, "Pattern: Strangler application" — microservices.io/patterns/refactoring/strangler-application.html. The phase of building the new implementation that coexists with the monolith behind the facade. In English.