Module 3: The Strangler Fig Pattern
The facade in front of the legacy
Overview
In the previous lesson you saw the complete cycle of the strangler fig and ran it in miniature. Now we start building it in phases, and the first is the most important and the one most people skip: putting the facade in front of the legacy. Before diverting a single request, before building a line of the new service, you need a place where the "old or new?" decision can be made. That place is the facade: a proxy that becomes the sole entry point of the traffic you're going to migrate.
The wonderful thing about the facade —and the reason it's the first step— is that it can be deployed without changing absolutely anything. The moment you install it, the traffic_percent is at 0: the facade receives all the requests and forwards them, all, to the legacy. The responses are identical to how they were before. The system behaves exactly the same. But now something that didn't exist before exists: a control point through which 100% of the traffic passes, where you can start to count, to measure, and —when you're ready— to divert. You installed the knob at 0; turning it is everything that follows.
This lesson executes it with a transparency test. We're going to compare, request by request, the response of the legacy called directly against the response of the same legacy through the facade, and verify they're identical byte for byte. That verification is what gives you the confidence to deploy the facade in production without fear: mathematically, you didn't change the behavior.
Connection with the module. Lesson 1 gave you the complete cycle; this one does its first phase: the facade as the entry point, deployed with zero risk. Lesson 3 does the second phase (build the new service alongside); lesson 4 does the third (divert the traffic by percentage) using this same facade with the traffic_percent now different from 0. Everything that follows in the module assumes this step is done: without the facade there's nowhere to decide old vs new. Notice the boundary: the facade here is an external proxy —it interposes at the endpoint's network boundary—. When there's no external boundary to interpose it at (an internal function of the monolith), the decision point is inserted in the code with an abstraction layer, and that's module 4. Here, GET /products is an endpoint: it has its boundary, and there the facade goes.
An analogy: the waiter who takes all the orders
Imagine a restaurant with a single kitchen —old, slow, but it works— and you want, over time, to replace it with a new kitchen. You can't change both kitchens at once in the middle of lunch service. The first thing you do has nothing to do with cooking: you put a waiter at the door who takes all the orders.
Before, the diners walked in and shouted their order straight at the old kitchen. Now, they all give their order to the waiter, and the waiter takes it to the kitchen. Did anything change for the diner? Nothing: they order the same, receive the same, with the same flavor and the same time, because the waiter simply takes each order to the old kitchen, as is. From the table, the restaurant is identical.
But for you, the restaurant owner, everything changed. Now you have a single point where all the orders pass. You can count how many orders come in, which dishes are ordered most, what time the peak hits. And —when the new kitchen is ready— you can tell the waiter: "the salad orders, take them to the new kitchen; the rest, to the old one." The waiter is the facade: it doesn't cook, it only decides which kitchen each order goes to. Installing it didn't change anyone's food; it gave you the control to change it later, dish by dish.
The software facade is that waiter. GET /products is the order. The kitchens are the legacy_catalog and the modern_catalog. And the fact that installing the waiter doesn't change anyone's food is exactly the transparency test we're going to execute.
Worked example: the transparency test of the facade at 0%
We're going to set up the facade and demonstrate that, at 0% diverted, it's transparent. We have the legacy_catalog —the old implementation of GET /products, the only one that exists today, with its query-filtering logic—. And we set up a StranglerFacade with traffic_percent=0 that, for now, forwards everything to the legacy but already keeps count of where each request went (its minimal observability). Then we pass a set of requests through both paths —direct legacy and via facade— and verify that the responses match.
# The facade is deployed WITHOUT changing behavior: it intercepts all the traffic
# of GET /products and today forwards it 100% to the legacy. It's transparent and auditable.
def legacy_catalog(request):
# The old implementation: the only one that exists today.
q = request.get("q", "")
products = ["ssd", "usb-hub", "webcam", "hdmi"]
if q:
products = [p for p in products if q in p]
return {"status": 200, "source": "legacy", "products": products}
# --- The strangler facade: sole entry point. traffic_percent=0 => all legacy. ---
class StranglerFacade:
def __init__(self, traffic_percent=0):
self.traffic_percent = traffic_percent
self.routed = {"legacy": 0, "modern": 0} # observability: counts the split
def handle(self, request):
# With traffic_percent=0 there's no new route yet: everything goes to the legacy.
# But NOW the point exists where tomorrow old vs new will be decided.
self.routed["legacy"] += 1
return legacy_catalog(request)
# --- Transparency test: the facade at 0% responds THE SAME as the direct legacy. ---
sample = [
{"id": 1, "path": "GET /products", "q": ""},
{"id": 2, "path": "GET /products", "q": "usb"},
{"id": 3, "path": "GET /products", "q": "hdmi"},
{"id": 4, "path": "GET /products", "q": "zzz"}, # no results
]
facade = StranglerFacade(traffic_percent=0)
print("Comparison: direct legacy vs through the facade (0% diverted)\n")
print(f"{'q':<9}{'direct legacy':<26}{'via facade':<26}{'equal?':>7}")
print("-" * 68)
all_equal = True
for req in sample:
direct = legacy_catalog(req)
through = facade.handle(req)
same = direct == through
all_equal = all_equal and same
q = req["q"] or "(empty)"
print(f"{q:<9}{','.join(direct['products']) or '-':<26}"
f"{','.join(through['products']) or '-':<26}{('YES' if same else 'NO'):>7}")
print("-" * 68)
print(f"\nAll responses identical: {all_equal}")
print(f"Split observed by the facade: {facade.routed}")
print("\n You deployed the facade without changing a single response (zero risk),")
print(" and now you have the control point to start diverting traffic.")
What to expect. When you run the file, the output is exactly this:
Comparison: direct legacy vs through the facade (0% diverted)
q direct legacy via facade equal?
--------------------------------------------------------------------
(empty) ssd,usb-hub,webcam,hdmi ssd,usb-hub,webcam,hdmi YES
usb usb-hub usb-hub YES
hdmi hdmi hdmi YES
zzz - - YES
--------------------------------------------------------------------
All responses identical: True
Split observed by the facade: {'legacy': 4, 'modern': 0}
You deployed the facade without changing a single response (zero risk),
and now you have the control point to start diverting traffic.
Read the equal? column: YES in the four rows. Without a query, with a query that filters (usb, hdmi), and with a query that returns nothing (zzz) —the edge case—, the response through the facade is identical to that of the direct legacy. And the line below confirms it in a single verification: All responses identical: True. That's transparency: the facade isn't a reimplementation, it's a pass-through. It takes the request and hands it as is to the old kitchen.
Notice also Split observed by the facade: {'legacy': 4, 'modern': 0}. The four requests went to the legacy, zero to the modern —logical, traffic_percent is 0 and a modern_catalog doesn't even exist yet—. But that counter is the seed of the observability: from minute one, the facade knows where each request went. When in lesson 4 you start diverting traffic, this same counter will tell you how many went to the new and how many to the old, without instrumenting anything else.
The lesson's point is in the combination of those two outputs: you changed the system's topology —now everything passes through a facade that wasn't there— without changing a single response or a single byte. In deployment terms, this is gold: you can put the facade in production, verify that the transparency test gives True, and know you didn't break anything. The facade is the lowest-risk step of the whole migration, and it's the one that enables all the others.
Deep dive: why the facade first, and what it must NOT do
The order matters, and it's no accident that the facade goes before the new service. If you built the modern_catalog first and only afterward the facade, you'd have a new service with no way to receive traffic gradually: you could only connect it with a big and risky change. The other way around —facade first— when the new service is ready, you already have the mechanism to give it traffic drop by drop. The facade is the diversion infrastructure; it's installed empty (at 0%) and filled later.
Here's the topology, before and after installing the facade:
BEFORE (no facade):
client ───────────────> legacy_catalog (direct call, no control)
AFTER (facade at 0%):
client ──> StranglerFacade ──> legacy_catalog
│
└── traffic_percent = 0 (all to the legacy, still)
└── routed = {legacy: N, modern: 0} (already counting)
The golden rule of the facade in this phase is one of discipline: the facade must do nothing but route. It doesn't validate, doesn't transform, doesn't add business logic, doesn't cache, doesn't "take the chance to fix along the way" the response format. As soon as the facade starts to do things, it stops being transparent —the transparency test would give False— and you lose the property that makes it safe. A facade that only routes can be deployed without fear; a facade that also transforms is a behavior change disguised as infrastructure, and that's where stranglers break. All the new logic lives in the modern_catalog (lesson 3), never in the facade.
This connects with the worst fate of a facade, which we'll see as a common mistake: the facade that, lesson after lesson, gets "one more little thing" added —a bit of validation here, a transformation there, a special case over there— until it becomes another monolith, as tangled as the one you wanted to replace. The facade has to stay thin. Its only reason to exist is to decide which kitchen each order goes to.
Common mistakes
Taking the chance to "improve along the way" the responses with the facade. What happens: when building the facade, the team sees an opportunity —"since everything passes through here, let's normalize the date format," "let's add this field we always lacked"—. Why it happens: the facade is a tempting point; it touches all the traffic, it seems the perfect place for cross-cutting changes. How to spot it: the transparency test stops giving True. If the response via facade differs from that of the direct legacy even in one field, the facade stopped being transparent. How to fix it: in the facade phase, zero behavior changes. The facade only routes. The improvements to the format or the new fields are the modern_catalog's job —there, yes, because the new service can behave differently and you control the percentage of traffic that sees it—. Keep transparency as a test that runs on every deploy of the facade: if it turns red, the facade got dirty.
Building the new service before the facade. What happens: the team gets excited about the reimplementation, builds a complete modern_catalog, and only at the end asks how to give it traffic gradually —and discovers there's no mechanism—. Why it happens: writing the new service is the fun part; installing a proxy that does nothing visible is the boring part, and it's postponed. How to spot it: you have a "ready" new service but the only way to activate it is a big change (point 100% of the traffic all at once), which is exactly the big-bang the strangler wants to avoid. How to fix it: the facade goes first, even if it feels useless at 0%. It's the infrastructure that enables the gradual diversion; without it, having the new service is of no use for migrating safely. Empty facade first, new service after, traffic drop by drop at the end.
Skipping the transparency test "because the facade only forwards." What happens: the team assumes the facade is trivially transparent and deploys it without verifying. Why it happens: "it only forwards, what could go wrong?". How to spot it: small differences nobody noticed —the facade re-serializes the JSON and changes the order of the keys, or loses a header, or converts a null into an empty string—. These differences are invisible until a client that depended on the exact format breaks. How to fix it: the transparency test isn't optional, it's cheap: compare the direct response against the facade's for a set of requests that covers the edge cases (empty query, no results, weird characters) and verify total equality. It's an afternoon's test that saves you an incident. That the facade "only forwards" is exactly what you have to prove, not assume.
Exercises
Exercise 1 — The transparent waiter. In the restaurant analogy, the waiter (facade) takes all the orders and takes them to the old kitchen. (a) What "transparency test" would the owner do to make sure that installing the waiter didn't change the diners' experience? (b) Give an example of something the waiter could "take the chance to do along the way" that would break transparency. (c) Why must that improvement, even if good, not be done by the waiter in this phase?
See solution
(a) The owner would compare, for the same order, the dish that comes out when the diner shouts directly at the old kitchen against the dish that comes out when it passes through the waiter: same dish, same flavor, same wait time, same presentation. If they're identical over a sample of orders —including the weird ones, like "no onion" or "to go"—, the waiter is transparent. It's exactly the direct legacy == via facade comparison of the example.
(b) The waiter could "take the chance to" add a side dish they always felt was missing, correct the doneness of the meat that "surely the diner wanted," or change the plate it's served on. Any of those things makes the via-waiter dish differ from the straight-to-kitchen dish: it breaks transparency.
(c) Because in this phase the goal is to install the control point without changing anyone's experience —that's exactly what makes it safe to deploy—. The improvements (the new side, the different format) are legitimate, but they belong to the new kitchen (modern_catalog), where you can control what percentage of diners it reaches. If the waiter improves the dishes, all the diners see the change at once, without a canary and without a fallback: it's the big-bang we wanted to avoid, snuck in through the back door.
Exercise 2 — Diagnose the transparency. A team deploys a facade and runs the transparency test. For three of four requests it gives YES, but for the request with an empty query (q="") it gives NO: the direct legacy returns ["ssd","usb-hub","webcam","hdmi"] and the facade returns ["hdmi","webcam","usb-hub","ssd"]. (a) Is the facade transparent? (b) What's the most likely cause? (c) Why could this break a client even though "the products are the same"?
See solution
(a) No. One row in NO is enough for the facade not to be transparent. Transparency is total equality of the response, not "the same elements in any order."
(b) The most likely cause is that the facade re-processes the list —for example, it deserializes it to a set and re-serializes it, or reorders it— instead of forwarding the legacy's response as is. The legacy returns the products in a fixed order (that of its internal list); the facade returned them in another order. The facade is touching the response, when it should pass it intact.
(c) Because a client can depend on the order: a mobile app that shows "the first product" as featured, an automated test that compares the exact response, or a contract with a partner that expects a stable order. "The same products in a different order" is a different response for any consumer that depends on the order, and in a legacy system you don't know who depends on what. The rule is strict for that reason: the facade forwards byte for byte, not "the equivalent." The fix is to make the facade return exactly what the legacy gave it, without re-serializing.
Exercise 3 — The facade's counter. The example's StranglerFacade keeps routed = {"legacy": N, "modern": 0} from the 0% phase. (a) What is that counter for if at 0% it always marks modern: 0? (b) What will that same counter tell you when the traffic_percent is at 30%? (c) Why is it useful for the observability to live in the facade and not in each service separately?
See solution
(a) At 0% the counter seems useless (modern always 0), but its value is that it's already installed: when you turn the knob, you won't have to add new instrumentation, it'll already be counting. Besides, from 0% it gives you useful data: how much total traffic the endpoint receives, which is the basis for sizing the new service. Installing the observability along with the facade —even if one side is at zero— is part of deploying the facade "ready to divert."
(b) At 30%, the counter will mark approximately {"legacy": 70% of the requests, "modern": 30% of the requests}. It's your direct measure of the real traffic split —not the percentage you asked for, but the one that actually happened— and the basis of lesson 7's burn-down: seeing legacy drop toward 0 is seeing the migration advance.
(c) Because the facade is the single point through which all the traffic passes: counting there gives you a complete and consistent view of the split, in one place. If the observability lived spread across each service, you'd have to add up metrics from two systems with different clocks and formats to know the split, and the legacy —which you didn't want to touch— would have to be instrumented. The facade centralizes the decision and the measurement of the decision: it knows where it sent each request because it sent it. It's the best place in the system to measure the migration's progress.
Summary and next step
In this lesson you did the first phase of the strangler fig: putting the facade in front of the legacy. You saw, with the waiter who takes all the orders, that the facade is a single entry point that decides which kitchen each one goes to without changing anyone's food. And you executed it with the transparency test: you compared, request by request, the direct legacy against the legacy via facade —including the edge case of a query with no results— and verified All responses identical: True. That's the lowest-risk step of the whole migration: you changed the system's topology without changing a single response, and in exchange you got the control point and the observability from which everything else will be possible.
Before moving on you should be able to: explain why the facade goes before the new service; state the golden rule of the facade (only routes, doesn't transform) and why breaking it breaks transparency; run a transparency test and know why a change of order already fails it; and describe what the facade's counter is for even at 0%.
Lesson 3 does the second phase: building the new service alongside. Now that the facade is in place and transparent, you're going to raise a modern_catalog with a different internal implementation —a store with stock, not the legacy's flat list— but the same public contract. You're going to execute the coexistence of the two, with a 20% canary, and verify that the new service meets the contract the facade expects. The legacy will stay intact; the new one will live beside it; and the facade —the one you just built— will be the one that chooses which to call.
Resources
- Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. Fowler describes "event interception" and the role of the proxy that interposes between the client and the old system: this lesson's facade. In English.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), ch. 3 — the section on the "HTTP proxy" and how to deploy the proxy without diverting traffic yet (equivalent to our facade at 0%) to reduce the change's risk. In English.
- Chris Richardson, "Pattern: Strangler application" — microservices.io/patterns/refactoring/strangler-application.html. The pattern's card, with the role of the "strangler facade" as the central component that intercepts the requests. In English.
- Paul Hammant, "Legacy Application Strangulation: Case Studies" (2013) — paulhammant.com/2013/07/14/legacy-application-strangulation-case-studies. Cases where the first move is always to interpose the interception point without changing behavior. In English.