PBO Tech
← All projects

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.

The demo’s UI after toggling the pricing slice to New: catalog still served by legacy-service, pricing now served by new-pricing-service, same four products, same prices

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.