PBO Tech
← All projects

Microservices Pattern Demo

Microservices patterns, built and running rather than just drawn as a diagram - a gateway, two services with their own databases, talking through events, with full tracing end to end.

The demo's UI: a form to place an order, the current inventory for widget, gadget and gizmo, and a table of orders placed so far

Reference architectures are usually diagrams and prose - accurate, but static. This is the same small set of integration patterns built as a real, runnable system instead: place an order through the UI and watch it cross two services, a message broker, and two databases, then follow its own trace straight into Jaeger to see exactly what happened and when.

flowchart LR
    person["Architect / developer (Person)"]
    system["Microservices Pattern Demo (Software System)"]
    jaeger[("Jaeger - trace storage (Software System)")]

    person -->|Places orders, browses inventory| system
    system -->|Exports OTLP spans| jaeger

    classDef person fill:#0a6b3a,stroke:#064a28,color:#fff
    classDef system fill:#1168bd,stroke:#0b4884,color:#fff
    class person person
    class system,jaeger system

There are five containers underneath that one system boundary, each doing one job - including a direct call from orders-service to inventory-service, which checks and reserves stock before an order is accepted. More on why that call exists further down, under Part 2.

flowchart LR
    client["Browser (UI)"]
    gw["API Gateway (nginx)"]
    orders["orders-service (FastAPI)"]
    inventory["inventory-service (FastAPI)"]
    broker[("Redis - broker + cache")]
    ordersDb[("orders.db (SQLite)")]
    inventoryDb[("inventory.db (SQLite) - stock + read model")]
    jaeger[("Jaeger - traces")]

    client --> gw
    gw -->|"/orders"| orders
    gw -->|"/inventory, /orders/summary"| inventory
    gw -->|"/jaeger/"| jaeger
    orders -->|reads/writes| ordersDb
    inventory -->|reads/writes stock, read model| inventoryDb
    inventory -->|"cache-aside"| broker
    orders -->|"reserve/release (sync)"| inventory
    orders -->|"publish: order_created + trace context"| broker
    broker -->|subscribe| inventory
    orders -.->|OTLP spans| jaeger
    inventory -.->|OTLP spans| jaeger

    classDef container fill:#1168bd,stroke:#0b4884,color:#fff
    class gw,orders,inventory,broker,ordersDb,inventoryDb,jaeger container

API Gateway. The browser only ever talks to nginx. It routes /orders and /inventory to the two services behind it and serves the UI itself - callers never call orders-service or inventory-service directly, and either can move, scale, or restart without the gateway’s routes changing.

Database-per-service. orders-service and inventory-service each keep their own SQLite file. Neither reads the other’s table directly; the only thing that crosses the boundary between them is the event described next. That separation is what lets either service be redeployed, or rebuilt entirely, without coordinating a shared schema change.

Publish-subscribe. When an order is placed, orders-service publishes an order_created event to Redis and returns - it has no idea whether anything is listening. inventory-service happens to be the current subscriber: it records a fulfilment entry when the event arrives, entirely on its own schedule - and, as of Part 3, that same entry doubles as a read model. Either service could be stopped, replaced, or scaled independently of the other. (Stock itself is handled differently as of Part 2 - see below.)

That last pattern has one well-known problem: Redis pub/sub carries no trace context on its own. Auto-instrumenting the publish/subscribe calls gives each one its own OpenTelemetry span, but without extra work, inventory-service’s reaction to an event shows up as a trace completely disconnected from the POST /orders that caused it - an easy thing to get wrong in any event-driven system, not specific to this demo. The fix: orders-service writes the current trace’s context into the event’s own JSON payload before publishing it, and inventory-service reads it back out and starts its own span as a child of that same trace, rather than a new one.

A Jaeger trace for a single order, showing spans from both orders-service and inventory-service under one connected timeline - including the Redis PUBLISH and the process_order_created span it triggered

The result is what that screenshot shows: one trace per order, spanning both services, not two unrelated ones that happen to share a timestamp. Every order placed through the demo links straight to its own trace - nothing to search for.

Two of the three patterns - API Gateway and Publish-Subscribe - are also named, vendor-documented entries in Architecture Governance Platform’s pattern glossary (Gateway Routing and Publisher-Subscriber respectively); this demo exists to show them actually working, not to replace those write-ups. Database-per-service has no equivalent in that glossary - it’s a general microservices decomposition pattern, not a cloud-vendor one. Architecture Governance Platform itself sits behind a login - the pattern catalogue is a public copy of the entries this demo is built from, plus the full 44-pattern glossary.

Part 2: resilience patterns adds Saga / Compensating Transaction, Circuit Breaker, and Retry - fixing this demo’s longest-standing known limitation (nothing ever checked whether stock existed before accepting an order), measured directly: a dependency failure drops from ~8 seconds of retries to under 100ms once the circuit breaker opens.

Part 3: CQRS and Cache-Aside adds a genuinely separate read model (GET /orders/summary, built from the same event as Part 2’s fulfilment log, not queried live) and a cache in front of GET /inventory - both demonstrated at the smallest scale that still means something, reusing Redis for a second, unrelated job rather than adding new infrastructure.

Want to see the same architecture on managed cloud services instead of Docker Compose? How to run event-driven microservices on Azure moves this exact system - gateway, per-service databases, event broker, tracing - onto Azure Container Apps, Service Bus, PostgreSQL Flexible Server and Application Insights, with the one limitation Redis pub/sub can’t fix: an event published while its consumer is down no longer gets lost. Its own companion page, Part 2 on Azure, adds the synchronous reservation call and two ways to get Circuit Breaker + Retry around it - porting the demo’s own code as-is, or Azure Container Apps’ built-in Dapr resiliency policies instead. Part 3 on Azure adds the read model and a cache - honestly, the one place this variant can’t reuse what Compose reuses, since Service Bus isn’t a cache the way Redis is.

The glossary provides an explanation for most of the technical terms mentioned in this demo.

Want to see a different pattern, built the same way? The Strangler Fig Demo is a separate, standalone demo: a legacy product catalog migrated one slice at a time behind a facade, with routing you can flip live. Not a Part of this demo - a different pattern, a different domain, kept deliberately separate.

Third in the series, Lost in Handover moves from integration patterns to an AI agent built on top of them - reasoning over a real parcel’s tracking history, honest about what it knows and what it doesn’t.