Module 3: The Strangler Fig Pattern
The final cutover and retiring the legacy
Overview
You reached the last phase of the strangler fig, and it's the one almost nobody completes: the final cutover and retiring the legacy. With lesson 6's gate you took the traffic_percent to 100 —all the traffic goes to the modern, the modern serves without fallbacks—. Many teams declare victory here and move on to something else. It's a mistake, and this lesson explains why: as long as the legacy stays on, the migration is not over. You keep paying for two systems, you keep maintaining two routes, and the monolith you wanted to shrink is still the same size. The strangler only pays off when the old tree dies and is retired; a fig that never chokes the host is just two trees competing for the same light, forever.
Retiring the legacy has a moment and a criterion. The moment is seen in the burn-down: the descending graph of calls to the legacy as you raise the traffic_percent. At 0% the legacy serves everything; at 50%, half; at 100%, if the modern is complete, zero. The retirement criterion is simple and strict: the legacy is turned off when the burn-down reaches zero —nobody calls it, not even via fallback—. And "retire" means something concrete and uncomfortable: delete the code. Not comment it out, not leave it in an if False, not "turn it off just in case but leave it there." Delete it, and simplify the facade that no longer has anything to decide.
This lesson executes the complete burn-down —0→10→25→50→75→100— until seeing the calls to the legacy reach zero, and applies the retirement gate. And it names the enemy this step combats: the eternal migration, the state in which the legacy has been "on just in case" for years with zero traffic, costing money and attention without giving anything in return.
Connection with the module. Lessons 4 to 6 diverted the traffic and decided when to go up; this one does the final cutover (phase 4) and retires the legacy. With it, the strangler cycle closes: facade (L2) → build alongside (L3) → divert 0→100 (L4-L6) → retire (L7). Lesson 8 integrates it all into Mercado's catalog. Notice the boundary: here we use the burn-down of calls to the legacy as the signal to cut, the last mile of the strangler. The general discipline of measuring the progress of a migration —fitness functions that prevent the legacy from growing back, progress metrics across a whole migration program, avoiding the eternal migration at system scale— is module 7. Here, the cutover of one slice; there, governing the complete migration.
An analogy: removing the scaffolding when the building stands on its own
When you build or remodel a building, you put up scaffolding: temporary structures that hold it up while the new building doesn't yet bear its own weight. The scaffolding is indispensable during the work. But it has a clear destiny: it's removed when the building stands on its own. A finished building with the scaffolding still up isn't "safer just in case" —it's a job nobody declared finished, that gets in the way, that costs to maintain, and that tells everyone who passes by that the work isn't done—.
Imagine a builder who, out of fear, leaves the scaffolding up forever "in case the building falls." The building has been standing for years, solid, full of people who live and work in it —and the scaffolding is still there, rusting, taking up the sidewalk, costing the scaffolding rental every month—. At some point you have to make the decision the builder postpones: the building stands on its own, remove the scaffolding.
The legacy, during the migration, is the scaffolding: it holds up while the modern doesn't yet bear all the traffic —it's the fallback, the net—. When the modern serves 100% without falling (burn-down at zero), the building stands on its own. Retiring the legacy is removing the scaffolding. Leaving it on "just in case" with zero traffic is the builder who never removes it: paying the rent of two systems forever, and never declaring the work finished.
Worked example: the burn-down and the retirement gate
We're going to execute the complete burn-down. The modern is already fixed (it handles all the cases, including webcam), so there are no fallbacks. We raise the traffic_percent through the ramp 0→10→25→50→75→100 and at each step we measure how many calls the legacy receives. At the end, the retirement gate checks whether the legacy received zero calls —and if so, it retires it—.
import zlib
def legacy_catalog(request):
return {"source": "legacy", "product": request["q"]}
def modern_catalog(request):
# Already fixed: handles all the cases, including "webcam".
return {"source": "modern", "product": request["q"]}
def bucket(request_id):
return zlib.crc32(str(request_id).encode()) % 100
def run(requests, traffic_percent):
calls = {"modern": 0, "legacy": 0}
for req in requests:
if bucket(req["id"]) < traffic_percent:
modern_catalog(req)
calls["modern"] += 1
else:
legacy_catalog(req)
calls["legacy"] += 1
return calls
N = 1000
requests = [{"id": i, "q": "webcam" if i % 10 == 0 else "ssd"} for i in range(1, N + 1)]
# --- Burn-down: the calls to the legacy must drop to ZERO before retiring it. ---
RAMP = [0, 10, 25, 50, 75, 100]
print(f"Burn-down of calls to the legacy during the ramp ({N} req/day)\n")
print(f"{'day':>4}{'traffic':>9}{'-> modern':>11}{'-> legacy':>11} calls to the legacy")
print("-" * 62)
last = None
for day, pct in enumerate(RAMP, start=1):
calls = run(requests, pct)
bar = "#" * (calls["legacy"] * 28 // N)
print(f"{day:>4}{pct:>8}%{calls['modern']:>11}{calls['legacy']:>11} |{bar:<28}|")
last = calls
# --- The retirement gate: the legacy is turned off only when NOBODY calls it. ---
print("-" * 62)
can_retire = (last["legacy"] == 0)
print(f"\nAt 100%: calls to the legacy = {last['legacy']}")
print(f"Retirement gate (calls_to_legacy == 0): "
f"{'RETIRE the legacy' if can_retire else 'DO NOT retire yet'}")
if can_retire:
print("\n -> The legacy module of catalog is deleted (not commented out: DELETED).")
print(" -> The facade is simplified: it no longer decides, always calls modern.")
print(" -> The catalog migration is DONE. One less slice of monolith.")
print("\n Leaving the legacy 'on just in case' with 0 calls is eternal")
print(" migration: you pay two systems forever. If the burn-down reached 0, turn it off.")
What to expect. When you run the file, the output is exactly this:
Burn-down of calls to the legacy during the ramp (1000 req/day)
day traffic -> modern -> legacy calls to the legacy
--------------------------------------------------------------
1 0% 0 1000 |############################|
2 10% 109 891 |######################## |
3 25% 271 729 |#################### |
4 50% 520 480 |############# |
5 75% 765 235 |###### |
6 100% 1000 0 | |
--------------------------------------------------------------
At 100%: calls to the legacy = 0
Retirement gate (calls_to_legacy == 0): RETIRE the legacy
-> The legacy module of catalog is deleted (not commented out: DELETED).
-> The facade is simplified: it no longer decides, always calls modern.
-> The catalog migration is DONE. One less slice of monolith.
Leaving the legacy 'on just in case' with 0 calls is eternal
migration: you pay two systems forever. If the burn-down reached 0, turn it off.
Look at the -> legacy column top to bottom, and watch the bar shrink: 1000, 891, 729, 480, 235, 0. That's a burn-down —the most satisfying metric of a migration, the visual evidence that the legacy is dying—. Each step of the traffic_percent is a bite out of the legacy's calls: at 50% the legacy serves ~480 (less than half), at 75% only ~235, and at 100% none. The bar, which started full, ends empty. The old tree stopped receiving sap.
When the burn-down reaches 0, the retirement gate activates: calls_to_legacy == 0 → RETIRE the legacy. And notice what "retire" means, in the three lines of the output:
- The legacy module is deleted —the code of
legacy_catalogis removed from the repository—. Not commented out, not left in anif False, not archived "just in case." Deleted. Dead code left "just in case" is debt: nobody maintains it, it rots, and the day someone reactivates it by mistake, it reintroduces the bugs it took so much work to leave behind. - The facade is simplified —it no longer has two routes to decide; it always calls the modern—. With the legacy out, the
if bucket < traffic_percentis superfluous: the facade becomes a direct pass-through to the modern, or disappears entirely if it no longer adds anything. The decision scaffolding is also removed. - The catalog migration is over —one less slice of monolith—. This is the strangler's payoff: the
catalogis no longer part of the legacy monolith; it's a service of its own, clean, understood, and tested. Mercado's monolith is one slice smaller.
The last line is the lesson's central warning: leaving the legacy on with 0 calls is eternal migration. It's the worst of states —you did all the work of diverting the traffic, reached 100%, and don't collect the prize— because you keep paying the operation, the maintenance, and the complexity of two systems, one of which serves no one. The burn-down reached zero: turn it off.
Deep dive: why retiring is so hard, and how to do it safely
If retiring the legacy is so clearly the right thing, why don't so many teams do it? Out of fear, and the fear has a logic: "what if there's a case we didn't see, a customer who calls the legacy via a weird route, a nightly process that depends on it?". That fear isn't irrational —it's the tacit knowledge of module 1—, and the answer isn't "turn it off and pray," it's retire with evidence:
traffic_percent ramp Retirement with evidence
───────────────────────── ──────────────────────────────────────
100% + fallback = 0 sustained the modern serves EVERYTHING, no holes
burn-down of calls = 0 NOBODY calls the legacy, not even via fallback
observed for N days it wasn't a quiet day: includes peaks,
month-end closes, nightly processes
The key is time: the burn-down has to be at zero for a period that covers the weird cases. A catalog can have different traffic on a Monday than on a Sunday, or a month-end peak, or a nightly process that only runs at 3 a.m. If you observe the burn-down at zero for a complete cycle —days, weeks if needed, including those weird moments— the evidence that nobody needs the legacy is solid. Fear is combated with data, not with permanent scaffolding.
And there's a net for the residual fear: the staged reversible retirement. Instead of deleting the legacy all at once, first you leave it in the repository but disconnected from the facade for a short period (the facade no longer calls it, but the code exists). If in that period nothing breaks, then you delete it. It's the difference between "turn off" (reversible, a grace period) and "retire" (delete, definitive). The grace period gives confidence; but it has an expiration date —a week, two— so as not to become the eternal migration we combat. "Disconnected for two weeks, and if nothing breaks, it's deleted" is a plan; "disconnected forever just in case" is the same problem under another name.
It's worth seeing the complete strangler cycle closed, now that you have all the pieces:
flowchart LR
A["1. facade<br/>at 0%"] --> B["2. modern<br/>alongside"]
B --> C["3. divert<br/>0->100 with gate"]
C --> D["4. burn-down<br/>= 0"]
D --> E["retire legacy<br/>(delete + simplify facade)"]
E --> F["catalog<br/>modernized"]
Common mistakes
Declaring the migration finished on reaching 100% without retiring the legacy. What happens: the traffic_percent reaches 100, the team celebrates, and the legacy stays on indefinitely. Why it happens: reaching 100% feels like the end, and retiring the legacy is extra work with no visible immediate reward (no user notices you deleted dead code). How to spot it: the legacy is still deployed, consuming resources and showing up in the dashboards, months after reaching 100%. How to fix it: the migration doesn't end at 100% traffic, it ends when the legacy is retired. The strangler's payoff —stop maintaining two systems, shrink the monolith— is only collected on retirement. Put the retirement as the "done" criterion from day one, and don't close the migration project until the legacy is deleted. 100% traffic is the second-to-last station, not the last.
Leaving the legacy "off but present" forever. What happens: the team disconnects the legacy from the facade but leaves the code in the repository "just in case," indefinitely. Why it happens: deleting code is scary (what if we need it?), and "off but present" seems like a prudent middle ground. How to spot it: there are legacy modules that have gone months or years without receiving a call but are still in the code, without an owner, without maintenance, showing up in every search and confusing every new developer. How to fix it: "off but present" is valid only as a grace period with an expiration date (one or two weeks to gain confidence). After that period without incidents, it's deleted. Permanent dead code is debt: it rots, becomes a trap (someone reactivates it and reintroduces old bugs), and contradicts the reason for the migration —to simplify—. A git with history keeps the code forever if you really need it; there's no need to leave it alive in main.
The facade that outlives the legacy and becomes another monolith. What happens: with the legacy retired, the facade —which no longer has anything to decide— stays, and over time logic gets added to it (validation, transformation, more routes) until it becomes a new tangled central point. Why it happens: the facade is already there, touching all the traffic, and it's a tempting place to put cross-cutting logic. How to spot it: the facade, which was supposed to be a pass-through, accumulates responsibilities and grows; it starts to look like the monolith you replaced. How to fix it: when the legacy is retired, the facade is also simplified or retired. If its only reason to exist was to decide between old and new, and there's no old anymore, the facade is superfluous: the clients can call the modern directly. If it's kept for another reason (authentication, routing to several services), it's kept thin, with a clear responsibility. A facade that becomes a monolith turns the migration into a circle: you replaced one monolith with another under a new name.
Exercises
Exercise 1 — Read the burn-down. In the output, the burn-down of calls to the legacy was 1000, 891, 729, 480, 235, 0 at the steps 0, 10, 25, 50, 75, 100. (a) Why does the legacy serve 480 at 50% and not exactly 500? (b) What does it mean that the last figure is exactly 0 and not, say, 100? (c) If at 100% the legacy had served 100 (via fallbacks), could you retire it? What would you do?
See solution
(a) Because the bucket distributes by hash: at 50%, approximately half of the ids fall below 50, but not exactly half. 520 came out to the modern and 480 to the legacy —close to 50/50, with the normal variation of a hash over 1000 ids—. What matters is the descending trend, not the exact number.
(b) That the last figure is 0 means the modern is complete: at 100% of routed traffic, there wasn't a single call to the legacy, not even via fallback. The modern successfully served all the requests, including the webcam ones (which in this example are already fixed). Zero is the retirement criterion: nobody needs the legacy. If it had come out 100, it would mean 100 requests still depended on the legacy —the modern has a hole—.
(c) You couldn't retire it. If at 100% the legacy serves 100 requests via fallback, those 100 requests are cases the modern doesn't handle; turning off the legacy would leave them without anyone to serve them (they'd become 500 errors). What you'd do is: identify what cases those 100 fallbacks are (with the observability of lesson 6), complete the modern so it handles them, verify that the fallback drops to 0, and then retire the legacy. The burn-down at zero is non-negotiable for the retirement: as long as someone calls the legacy —even via fallback—, the legacy is needed.
Exercise 2 — Retire with evidence. A team reached 100% traffic on the modern, with fallback at 0, and wants to retire the legacy today. (a) What do they need to verify before deleting? (b) Why does the observation time matter so much for the retirement? (c) Describe a staged reversible retirement plan.
See solution
(a) They need to verify that the burn-down at zero holds over time, covering the weird cases. A single day at 100% with fallback 0 isn't enough: maybe that day the nightly process that calls the catalog didn't run, or it wasn't a month-end close, or a customer who uses a weird route didn't come in. Retiring the same day you reach 100% is retiring with insufficient evidence.
(b) Because a system's traffic has seasonality: a Monday isn't a Sunday, a normal day isn't a month-end close, the day isn't the small hours. The legacy could have a case that's only exercised at those weird moments (a monthly report, a nightly batch, a partner flow that calls once a week). Observing the burn-down at zero for a complete cycle —including those moments— is what gives solid evidence that nobody needs the legacy, not just "nobody needed it today."
(c) A staged reversible plan: (1) Disconnect the legacy from the facade —the facade can no longer route to the legacy, but the code stays in the repository—. (2) Observe for a period with an expiration date (one or two weeks, covering a month-end close and the nightly processes). If something breaks, reconnecting is trivial (the code is there). (3) If nothing breaks in the period, delete the legacy's code and simplify the facade. Stage (1)-(2) gives a reversible grace period that combats fear; the expiration date of (2) prevents that period from becoming the eternal migration. The git history keeps the deleted code in case one day it's really needed.
Exercise 3 — Diagnose the eternal migration. In Mercado, the legacy catalog has been disconnected from the facade for eight months —zero calls— but is still deployed and in the code. (a) What state is this migration in? (b) Name three concrete costs of leaving it like this. (c) What argument would you give to retire it, and how would you answer the "what if we need it?"?
See solution
(a) It's in eternal migration: technically the traffic was migrated (zero calls to the legacy), but the legacy was never retired, so the migration never closed. It's the worst state —all the work done, the prize uncollected—. Eight months of zero calls is more than enough evidence that nobody needs it; leaving it is pure fear, not prudence.
(b) Three concrete costs:
- Operation cost: the legacy is still deployed, consuming servers, memory, licenses —resources paid every month for a system that serves no one—.
- Cognitive cost: every new developer who explores the code finds the legacy
catalog, doesn't know it's dead, and wastes time understanding it or —worse— modifies it thinking it's alive. Dead code confuses and wastes attention. - Risk cost: unmaintained dead code rots (unupdated dependencies, unpatched vulnerabilities) and is a trap: someone could reactivate it by mistake and reintroduce the bugs and behaviors the migration left behind.
(c) The argument: eight months of zero calls is overwhelming evidence that the legacy isn't needed; the three costs above are paid every day it stays there; and the migration's payoff —shrink the monolith, stop maintaining two systems— isn't collected until it's retired. To the "what if we need it?", the answer is twofold: first, eight months of data say no; second, the code isn't lost —the git history keeps it—, so in the unlikely case of needing it, it's recovered. Retiring it isn't throwing the code in the garbage: it's taking it out of the live system. The reversibility exists (git); the eternal migration doesn't have to.
Summary and next step
In this lesson you did the last phase of the strangler fig: the final cutover and retiring the legacy. You saw, with the scaffolding removed when the building stands on its own, that leaving the legacy on "just in case" isn't prudence but eternal migration —the work done without collecting the prize—. And you executed it: the burn-down of calls to the legacy dropping 1000→891→729→480→235→0 over the ramp, and the retirement gate activating on reaching zero. Retiring meant three concrete things: deleting the legacy's code (not commenting it out), simplifying the facade that no longer decides, and declaring the catalog one less slice of monolith. And you learned to retire with evidence —burn-down at zero sustained for a cycle that covers the weird cases— and in a staged reversible way, to combat the fear without falling into permanence.
Before moving on you should be able to: read a burn-down and understand why zero is the retirement criterion; explain why 100% traffic isn't the end, but retiring the legacy is; argue why "retire" is delete and not "turn off just in case"; and diagnose an eternal migration and defend closing it against the "what if we need it?".
Lesson 8 is the capstone: modernizing Mercado's catalog end to end, integrating the whole module into a single executed simulation. You're going to set up a StranglerFacade that combines the four phases —facade + new service alongside + diversion by percentage with fallback + observability + gate + burn-down + retirement— and run it as a day-by-day migration journal: the gate raises the percentage only when the new route is healthy, the team deploys the fix mid-ramp, and on reaching 100% with the burn-down at zero the legacy is retired. The deliverable will be the migration plan by slices, the executed code, and the justification of why incremental beat the rewrite —the close of the module and the bridge to M4 and M5—.
Resources
- Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. Fowler emphasizes that the strangler's goal is to replace, not coexist: the old system must die. The conceptual basis of why retiring is the phase that closes the pattern. In English.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), ch. 3 — the last mile of the migration: retiring the functionality from the monolith once the new service serves it completely, and why leaving it "just in case" perpetuates the problem. In English.
- Chris Richardson, "Pattern: Strangler application" — microservices.io/patterns/refactoring/strangler-application.html. The pattern's result: the monolith shrinks until it disappears (or is left minimal) as its functionality is retired. In English.
- Paul Hammant, "Legacy Application Strangulation: Case Studies" (2013) — paulhammant.com/2013/07/14/legacy-application-strangulation-case-studies. Real cases where the strangulation is completed —the legacy is turned off— versus those that stay half done. The difference between strangling and coexisting. In English.