Strangler Fig Demo
The Strangler Fig migration pattern, built and running rather than just drawn as a diagram - a facade, a legacy system, and two purpose-built replacements, with routing you can flip live and watch take effect.
Most explanations of Strangler Fig are a single diagram: a facade, an arrow to “legacy,” a dotted arrow to “new.” This is the same pattern built as a runnable system instead - flip a slice’s routing live and watch, in the very next response, which service actually answered.
flowchart LR
person["Visitor (Person)"]
system["Strangler Fig Demo (Software System)"]
person -->|Browses the catalog, toggles routing| system
classDef person fill:#0a6b3a,stroke:#064a28,color:#fff
classDef system fill:#1168bd,stroke:#0b4884,color:#fff
class person person
class system system
Underneath that one system boundary sit four containers: a facade, the legacy system it’s migrating away from, and one purpose-built replacement for each of the two slices being pulled out of it.
flowchart LR
client["Browser (UI)"]
facade["Strangler Facade (FastAPI)"]
legacy["legacy-service - catalog + pricing (FastAPI)"]
newCatalog["new-catalog-service (FastAPI)"]
newPricing["new-pricing-service (FastAPI)"]
client --> facade
facade -->|"catalog: legacy or new"| legacy
facade -->|"catalog: legacy or new"| newCatalog
facade -->|"pricing: legacy or new"| legacy
facade -->|"pricing: legacy or new"| newPricing
classDef container fill:#1168bd,stroke:#0b4884,color:#fff
class facade,legacy,newCatalog,newPricing container
The facade is the only address any caller knows. legacy-service, new-catalog-service and
new-pricing-service are never called directly - the facade decides, per slice, which of them
actually answers. A caller browsing the catalog has no way to tell whether that request reached
the old monolith or its brand-new replacement, which is exactly what a real migration needs to be
true.
Two slices, each migrated on its own schedule. Catalog - listing products and looking one up - and pricing are routed independently. Flipping pricing from legacy to new doesn’t touch catalog’s routing at all; each slice moves only when it’s ready, with the legacy system still answering everything that hasn’t moved yet.

That screenshot is the moment the pattern is supposed to make possible: the catalog is still
answered by the legacy system, pricing has already moved - and the price itself hasn’t changed,
because both backends hold the same data. Identical response shape, bar one field. Every
backend includes a served_by field naming itself; every other field - id, name, price - is
identical whichever one answered. That’s not incidental to the demo; it’s the actual mechanism the
whole pattern depends on. If a new service’s response ever drifted from the legacy one it’s
replacing, nothing could be migrated onto it without a caller noticing.
This demo deliberately doesn’t carry over the Microservices Pattern Demo’s own focus: no OpenTelemetry tracing, no retry or circuit-breaking on the proxied calls, no authentication on the endpoint that flips live routing. Barebones, so the one pattern it exists to show stays the only thing happening. For resilience and observability demonstrated properly, see the Microservices Pattern Demo - a separate demo, a separate pattern, a separate domain (orders and inventory, rather than a product catalog).
Want to see a pattern about trusting - or not trusting - what a system tells you, rather than how it’s built? Lost in Handover is the third in the series: an AI agent reasoning over a real parcel’s tracking history, honest about what it knows, what it’s inferring, and what it doesn’t know at all.