Module 3: The Strangler Fig Pattern
Project: put Mercado's catalog behind a strangler facade
Overview
You reached the module's capstone. In the previous seven lessons you built the strangler fig pattern piece by piece: the transparent facade (L2), the new service alongside (L3), the diversion by percentage with fallback (L4), the three diversion strategies (L5), the observability and the promotion gate (L6), and the final cutover with burn-down and retirement (L7). In this project you put it all together in a single executed simulation: you modernize Mercado's catalog end to end, with a StranglerFacade that integrates the four phases, run as a day-by-day migration journal.
The journal is the most honest way to see a complete strangler, because it shows what the isolated lessons don't: how the pieces work together over time. You're going to see the gate raise the traffic_percent only when the new route is healthy, a bug that appears mid-ramp and stops the promotion, the team that deploys the fix, the ramp that resumes, the burn-down that reaches zero, and —the prize— the legacy that is retired. It's the whole film, not the loose photos.
And this project has a deliverable, like every capstone: (1) the migration plan by slices —why the catalog, and in what order the rest—, (2) the executed code —the complete StranglerFacade run with its literal output—, and (3) the justification of why this incremental path beat the rewrite that module 1 dismantled. When you finish, you'll hold in your hands a migration of one real slice, from beginning to end, that you can defend with numbers.
Connection with the module. This project integrates lessons 2 to 7 into a continuous execution. It closes module 3 and prepares the two that follow: module 4 (branch by abstraction) teaches the variant for when there isn't an external boundary to put the facade —the migration inside the code—; and module 5 (extract a service) takes the modern we build here alongside and takes it out to its own process with an anti-corruption layer. Notice the capstone's boundary: here we modernize the catalog with the strangler technique (external traffic diversion). How the modern is built inside and where it ends up (microservice, events) is for the style guides; migrating its data without downtime is module 6; and measuring the progress of Mercado's complete migration (all the slices) is module 7. This capstone is one slice, executed whole.
The migration plan by slices
Before the code, the plan —because a strangler without a plan is just a proxy—. Mercado is a monolith with four modules: catalog, orders, payments, shipping. We don't migrate them all at once; we choose a first slice and take it end to end before touching the next. Module 1 already made this choice (the catalog: high value, low coupling, a good candidate); here we execute it.
Slice Why in this order State in this project
────────── ───────────────────────────────────────────── ──────────────────────
catalog high value, low coupling (writes little; <- THIS slice,
reads a lot); low risk if the canary fails end to end
orders depends on catalog; migrated afterward next
payments critical (money); migrated more carefully later
shipping coupled to orders; last last
The principle of the plan by slices: one slice at a time, end to end. Don't open the orders strangler while the catalog one is half done —you'd end up with four migrations at 40%, none collected—. You take the catalog all the way to retiring its legacy, you collect the prize (one less slice of monolith), and then you start orders. Each complete slice reduces the monolith and teaches you something for the next.
An analogy: moving house room by room, finishing each one
Think about moving your whole home from an old house to a new one, but with the rule that you can never be left without a functional room. You don't load everything on a truck one day (that's the big-bang). You move one room at a time, and finish it before starting the next: first the kitchen —you set it up completely in the new house, use it for a few days, confirm that everything works (the water, the gas, the appliances)—, and only when the new kitchen fully works do you empty and dismantle the old kitchen. Only then do you start with the bedroom.
If you moved the four rooms "a little each" at once, you'd live for weeks with four half-assembled rooms, none usable, boxes everywhere. Finishing each room before starting the next gives you, at each step, a room completely functional in the new house and a room completely empty in the old one —real, collected progress—.
The old house is the monolith; the rooms are the slices (catalog, orders...). Setting up the new kitchen and using it for a few days before dismantling the old is the strangler's ramp with its burn-down. And dismantling the old kitchen when the new one works is retiring the legacy. This project moves the kitchen —the catalog— completely, end to end.
Worked example: the catalog's migration journal, executed
Here's the complete StranglerFacade, integrating the module's four phases, run as a day-by-day journal. The facade routes by percentage with fallback (L4), observes the two routes and computes the error_rate (L6), and a gate raises the traffic_percent through the ramp only when the new route is healthy. The modern starts with the webcam bug (v1) and the team deploys the fix (v2) on day 3. When the ramp reaches 100% and the burn-down hits zero (fallback 0), the legacy is retired (L7).
# ============================================================================
# Capstone: modernize Mercado's catalog with a strangler facade,
# end to end and EXECUTED. Facade + new service alongside + diversion by
# percentage with fallback + observability + gate + burn-down + retirement.
# ============================================================================
import zlib
def legacy_catalog(request):
return {"source": "legacy", "product": request["q"]}
def modern_catalog(request, version):
# v1 fails on "webcam" (out-of-stock not implemented); v2 fixes it.
if version == 1 and request["q"] == "webcam":
raise ValueError("modern v1: unhandled out-of-stock path")
return {"source": "modern", "product": request["q"]}
def bucket(request_id):
return zlib.crc32(str(request_id).encode()) % 100
class StranglerFacade:
def __init__(self):
self.traffic_percent = 0
self.modern_version = 1 # starts with the bug
self.retired = False
def handle_batch(self, requests):
obs = {"modern_ok": 0, "fallback": 0, "legacy": 0}
for req in requests:
goes_modern = (not self.retired) and bucket(req["id"]) < self.traffic_percent
if self.retired:
goes_modern = True # no more legacy: everything goes to modern
if goes_modern:
try:
modern_catalog(req, self.modern_version)
obs["modern_ok"] += 1
except Exception:
obs["fallback"] += 1
legacy_catalog(req) # the old route is the safety net
else:
legacy_catalog(req)
obs["legacy"] += 1
return obs
def error_rate(obs):
attempted = obs["modern_ok"] + obs["fallback"]
return (obs["fallback"] / attempted * 100) if attempted else 0.0
def pct_on_new_route(obs):
total = obs["modern_ok"] + obs["fallback"] + obs["legacy"]
return obs["modern_ok"] / total * 100 if total else 0.0
# --- Fixed catalog traffic: 1 in every 10 queries "webcam". ---
N = 1000
requests = [{"id": i, "q": "webcam" if i % 10 == 0 else "ssd"} for i in range(1, N + 1)]
RAMP = [0, 10, 25, 50, 75, 100]
GATE = 1.0 # promote only if the new route's error_rate <= 1%
facade = StranglerFacade()
ramp_idx = 0
FIX_DAY = 3 # the team deploys modern v2 (fixed) this day
print("Migration journal of Mercado's catalog (gate = error_rate <= 1.0%)\n")
print(f"{'day':>4}{'traffic':>9}{'ver':>5}{'ok':>6}{'fb':>5}{'legacy':>8}"
f"{'err%':>7}{'new%':>7} decision")
print("-" * 74)
for day in range(1, 9):
if day == FIX_DAY:
facade.modern_version = 2 # the webcam bug is fixed
used_pct = facade.traffic_percent # the % it runs with THIS day
obs = facade.handle_batch(requests)
er = error_rate(obs)
newp = pct_on_new_route(obs)
# Gate: if the new route is healthy, go up to the next step of the ramp.
decision = "-"
if facade.retired:
decision = "legacy RETIRED"
elif facade.traffic_percent == 100 and obs["fallback"] == 0:
facade.retired = True
decision = "burn-down=0 -> RETIRE legacy"
elif er <= GATE and ramp_idx < len(RAMP) - 1:
ramp_idx += 1
facade.traffic_percent = RAMP[ramp_idx]
decision = f"gate OK -> go up to {RAMP[ramp_idx]}%"
elif er > GATE:
decision = "gate FAILS -> HOLD (fix)"
print(f"{day:>4}{used_pct:>8}%{facade.modern_version:>5}"
f"{obs['modern_ok']:>6}{obs['fallback']:>5}{obs['legacy']:>8}"
f"{er:>6.1f}%{newp:>6.1f}% {decision}")
print("-" * 74)
print("\nDeliverable: Mercado's catalog ended up 100% on the new service,")
print("the legacy was retired (0 calls), and each step was raised only when")
print("the new route was healthy. Incremental, measured, and reversible at every step.")
What to expect. When you run the file, the output is exactly this:
Migration journal of Mercado's catalog (gate = error_rate <= 1.0%)
day traffic ver ok fb legacy err% new% decision
--------------------------------------------------------------------------
1 0% 1 0 0 1000 0.0% 0.0% gate OK -> go up to 10%
2 10% 1 99 10 891 9.2% 9.9% gate FAILS -> HOLD (fix)
3 10% 2 109 0 891 0.0% 10.9% gate OK -> go up to 25%
4 25% 2 271 0 729 0.0% 27.1% gate OK -> go up to 50%
5 50% 2 520 0 480 0.0% 52.0% gate OK -> go up to 75%
6 75% 2 765 0 235 0.0% 76.5% gate OK -> go up to 100%
7 100% 2 1000 0 0 0.0% 100.0% burn-down=0 -> RETIRE legacy
8 100% 2 1000 0 0 0.0% 100.0% legacy RETIRED
--------------------------------------------------------------------------
Deliverable: Mercado's catalog ended up 100% on the new service,
the legacy was retired (0 calls), and each step was raised only when
the new route was healthy. Incremental, measured, and reversible at every step.
Read the journal day by day, because each row is a real migration decision and each column one of the module's pieces working together.
Day 1 — The facade starts at 0% (everything to the legacy, legacy=1000). It's the state of lesson 2: the facade in place, transparent, diverting nothing. The gate sees an error_rate of 0% (there's no new route exercised yet) and authorizes going up to the first step: go up to 10%.
Day 2 — Now at 10% with the modern v1 (with bug). Of the 109 requests routed to the modern, 99 succeed and 10 fall into fallback (the webcam ones). The error_rate is 9.2%, far above the 1% threshold. The gate decides: HOLD (fix). Here's the measured heart of the strangler: the system didn't raise the percentage because the new route wasn't healthy. And notice the protection: those 10 failures didn't affect any user (the fallback served them with the legacy); they only stopped the promotion. The canary did its job —it detected the problem with 10% of the traffic exposed—.
Day 3 — The team deploys modern v2 (fixed). At 10%, the 109 requests routed to the modern now all succeed: fallback=0, error_rate 0.0%. The gate: go up to 25%. The fix translated, immediately and automatically, into permission to advance. There was no meeting or hunch; the number dropped and the gate opened.
Days 4, 5, 6 — The ramp resumes without friction: 25% → 50% → 75% → 100%. At each step the error_rate is 0%, the gate authorizes, and the new% (percentage of traffic on the new route) rises: 27.1%, 52.0%, 76.5%. It's the burn-down of lesson 7 live: the legacy column drops 729 → 480 → 235.
Day 7 — At 100%, the 1000 requests go to the modern, all successful (ok=1000, fb=0, legacy=0). The burn-down hit zero: nobody calls the legacy, not even via fallback. The retirement gate activates: burn-down=0 → RETIRE legacy. The modern is complete (v2 handles webcam), so turning off the legacy is safe.
Day 8 — The legacy is RETIRED. The facade no longer decides anything; everything goes to the modern by definition. Mercado's catalog is one less slice of monolith.
The journal tells the strangler's whole story in eight rows: a facade that changed nothing on starting, a canary that caught a bug without exposing it to the users, a gate that stopped and then authorized based on a number, a burn-down that reached zero, and a retired legacy. Each step was incremental (one step at a time), measured (the gate decided by the error_rate) and reversible (on any day you could lower the percentage). That's what module 1 promised incremental would give and the rewrite wouldn't.
The capstone deliverable
A capstone delivers artifacts, not just understanding. Here are the three:
1. The migration plan by slices. Mercado's monolith is migrated slice by slice, starting with the catalog (high value, low coupling) and continuing with orders, payments, shipping in that order, one at a time and end to end. Each slice goes through the strangler's four phases until retiring its legacy before starting the next. (The plan's table is above.)
2. The executed code. The complete StranglerFacade —facade + modern alongside + diversion by percentage with fallback + observability + gate + burn-down + retirement— run with its literal output, the eight-day journal. It's not pseudocode or a description: it's the simulated migration, reproducible, that you can run and modify (change the FIX_DAY, the GATE, the ramp, and observe how the journal changes).
3. The justification: why incremental beat the rewrite. Module 1 measured that a rewrite of the monolith would deliver zero value for years and would risk the whole system at once. This project shows the alternative executed: the catalog was modernized without shutting down the business (the system served traffic all eight days), without exposing the users to the bugs (the fallback caught the day-2 problem), with each step measured (the gate), and with the prize collected (the legacy retired, the monolith smaller). Where the rewrite demanded two years of silence and a total bet, the strangler delivered a modernized slice with bounded risk at each step. That's the justification, and now you can back it with an executed journal.
Common mistakes
Opening several slices at once instead of finishing one. What happens: excited, the team puts strangler facades on catalog, orders, and payments at the same time. Why it happens: it seems faster to advance on everything at once, and each facade at 0% feels cheap. How to spot it: there are three or four migrations "in progress," none close to retiring its legacy; the monolith didn't shrink at all because no legacy was turned off. How to fix it: one slice at a time, end to end. Finish the catalog —all the way to retiring its legacy— before touching orders. A migration's progress isn't measured in "how many slices you started" but in "how many legacies you retired." Four migrations at 40% are worth zero retired legacies; one migration at 100% is worth one. The discipline of finishing is what collects the prize.
A gate that in practice always approves. What happens: the team puts a gate but with a threshold so lax (error_rate ≤ 20%) that it never stops anything, or ignores it when in a hurry. Why it happens: the gate that stops is uncomfortable —on day 2 of the journal, the gate stopped the migration, and that annoys whoever is in a hurry—. How to spot it: the traffic_percent rises at each step without exception, even when the error_rate was high; the gate is decorative. How to fix it: the gate only serves if it can really say "no." A realistic threshold (error_rate ≤ 1%, like the journal) stops when the modern isn't healthy —and that stop is what avoids exposing the users—. If the gate never stops, it's not a gate, it's an ornament; and without a real gate, you raise the percentage by hunch, which is exactly what lesson 6 combated. The gate's value is in the times it says "no."
Declaring the capstone finished on day 7 (100%) without reaching day 8 (retired). What happens: on seeing the traffic_percent at 100% and the burn-down at zero, the team considers the migration done and doesn't execute the retirement. Why it happens: 100% feels like the end, and the retirement (deleting the legacy, simplifying the facade) is work with no visible reward. How to spot it: the journal stops at day 7; the legacy is still deployed indefinitely even though nobody calls it. How to fix it: the capstone —like the strangler— ends on day 8, with the legacy retired, not on day 7. The whole module insisted on this: the strangler's payoff is retiring the legacy. A capstone that reaches 100% but doesn't retire is the eternal migration of lesson 7, and it leaves the monolith the same size. The slice is migrated when its legacy is deleted, not when its traffic reached 100%.
Exercises
Exercise 1 — Explain day 2. In the journal, on day 2 the gate decided HOLD (fix) instead of raising the percentage. (a) What number triggered that decision? (b) What would have happened to the users if the gate had gone up to 25% anyway? (c) Why is it a good sign that the migration stopped that day?
See solution
(a) The error_rate of 9.2%. The modern v1 failed on 10 of the 109 requests it got (the webcam ones), and 10/109 = 9.2%, far above the gate's threshold (1.0%). That number triggered the HOLD.
(b) If the gate had gone up to 25% with the modern v1 still broken, more webcam requests would have been routed to the modern and would have fallen into fallback —more double-latency load, and the problem scaling instead of contained—. The users would still be protected by the fallback (nobody would see a 500 error), but you'd be exposing the bug to more traffic without having fixed it, and getting closer to the point where the volume of fallbacks becomes a performance problem. Going up with the new route sick is the opposite of the canary.
(c) Because it means the safety system worked: the canary detected the modern's bug with only 10% of the traffic exposed, the fallback protected the users from seeing it, and the gate prevented the migration from advancing on a broken base. A migration that stops faced with a real problem is a healthy migration —it's using its instruments—. The dangerous thing would be the opposite: raising the percentage ignoring the error_rate. The HOLD on day 2 is the pattern doing exactly what it promises: stopping before exposing, not after.
Exercise 2 — Change a parameter and predict. Without running the code, predict how the journal would change if the team deployed the fix (FIX_DAY) on day 5 instead of day 3. (a) What would happen on days 2, 3, 4? (b) Would the migration reach 100% within the 8 days? (c) What does this tell you about the relationship between fixing fast and migrating fast?
See solution
(a) With FIX_DAY=5, the modern stays on v1 (with bug) on days 2, 3, and 4. Day 2 at 10% gives error_rate 9.2% → HOLD. Day 3, still v1 at 10%, gives 9.2% again → HOLD again. Day 4, still v1 at 10%, another HOLD. The migration stays nailed at 10% as long as the bug isn't fixed: the gate doesn't let it go up. Three days lost on the same step.
(b) Hardly. The fix arrives on day 5 (v2, error_rate 0% → go up to 25%). After that it needs days 6 (25%→50%), 7 (50%→75%), 8 (75%→100%)... and it would reach 100% on day 8, with no days left to verify the burn-down and retire. The retirement would fall outside the 8-day window. With the late fix, the migration doesn't manage to close within the same horizon.
(c) That the migration's speed is limited by the new route's health, not by the desire to advance. The gate doesn't let the percentage go up while the modern fails, so every day the bug goes unfixed is a day the migration doesn't advance. Fixing fast is migrating fast: the bottleneck isn't the ramp, it's the modern's quality. This reinforces lesson 3 (build the modern well, from the contract and the characterization tests) and lesson 6 (the gate that measures): a healthy modern advances on its own; a buggy one gets stuck on the first step.
Exercise 3 — The next slice's plan. The catalog is migrated (legacy retired). Now it's the next slice's turn. (a) According to the plan, which one is next and why? (b) What of this catalog migration would you reuse for the next, and what would you expect to be different? (c) Why wasn't payments the first slice?
See solution
(a) According to the plan, next is orders. It was chosen after the catalog because orders depends on the catalog (it needs product data), so having the catalog already modernized and stable makes it easier to migrate orders on a clean base. Besides, with the catalog complete, the team already went through the whole strangler once and learned the process.
(b) You'd reuse all the strangler machinery: the StranglerFacade pattern (route by percentage with fallback), the observability of the two routes, the promotion gate, and the burn-down retirement criterion. The technique is the same for any slice. You'd expect to be different: the orders modern implementation (different business logic, more writes than the catalog that almost only reads), maybe a different diversion strategy (orders write data, so the fallback and consistency are more delicate than in a read-only catalog), and probably a slower ramp because it's more critical. The choreography is the same; the content changes.
(c) Because payments handles money, and it's the most critical module: a failure of the modern in payments, even if the fallback catches it, has much more serious consequences than a failure in the catalog (a double charge, a lost payment). The plan's principle is to start with the slice of highest value and lowest risk to learn the process where an error is forgiven —the catalog, which almost only reads and where a failed canary doesn't cost money—, and leave the critical slice (payments) for when the team already masters the strangler and can migrate it with the slowest ramp and the strictest checks. You practice where it's cheap to make a mistake before touching where it's expensive.
Summary and next step
In this capstone you integrated the whole module into a single executed migration: you modernized Mercado's catalog end to end with a StranglerFacade that combined the four phases —facade, new service alongside, diversion by percentage with fallback and gate, and retirement by burn-down—, run as an eight-day journal. You saw the complete system work together: a canary that caught a bug on day 2 without exposing it to the users, a gate that stopped the migration and then resumed it based on a number, a burn-down that reached zero, and a legacy retired on day 8. And you produced the capstone deliverable: the migration plan by slices, the executed code, and the justification —backed by the journal— of why incremental beat the rewrite.
With this you close module 3. You master the strangler fig pattern: you know how to put a transparent facade, build the new one alongside, divert the traffic by the three strategies with fallback, observe the two routes and decide with a gate, and retire the legacy when the burn-down reaches zero, all incremental, measured, and reversible.
What follows extends this technique to the cases the external strangler doesn't cover. Module 4 (branch by abstraction) teaches what to do when there isn't an external boundary to put the facade —when the thing you want to migrate is an internal function of the monolith, with no endpoint or possible proxy—: you insert an abstraction layer in the code and migrate the implementation behind it, with the two coexisting behind a flag, without a long-lived git branch. And module 5 (extract a service) takes the modern we build here alongside and takes it out to its own process, with an anti-corruption layer that translates between the old model and the new one and with the ownership of the data well defined. The strangler gave you the way to move the traffic; the modules that follow give you the way to move the code and the data behind it.
Resources
- Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. The founding text of the pattern this capstone executes from beginning to end. Re-read it now that you have the complete mechanics: every sentence will have a concrete referent. In English.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), the whole ch. 3 — the comprehensive reference for migrating a monolith by slices with the strangler: choose the first slice, divert the traffic, and retire the functionality from the monolith. The map of the capstone and the slices that follow. In English.
- Chris Richardson, "Pattern: Strangler application" — microservices.io/patterns/refactoring/strangler-application.html. The pattern's card seen now as a whole: the result is a monolith that shrinks slice by slice until it disappears. In English.
- Paul Hammant, "Legacy Application Strangulation: Case Studies" (2013) — paulhammant.com/2013/07/14/legacy-application-strangulation-case-studies. Real cases of complete strangulation migrations, with the successes and the stumbles —the practical complement to this capstone's simulation—. In English.