← Microservices Pattern Demo · builds on Part 2: resilience patterns
Part 3: CQRS and Cache-Aside
Part 2 fixed the demo’s biggest gap - nothing checked whether stock existed before accepting an
order. This round adds two more patterns to the same orders/inventory system: CQRS, a
genuinely separate read model built from an event the demo already publishes, and
Cache-Aside, a cache layered onto inventory-service’s busiest read.
What’s new
flowchart LR
client["Browser (UI)"]
gw["API Gateway (nginx)"]
orders["orders-service (FastAPI)"]
inventory["inventory-service (FastAPI)"]
broker[("Redis - broker + cache")]
inventoryDb[("inventory.db (SQLite) - stock + read model")]
client --> gw
gw -->|"/inventory, /orders/summary"| inventory
inventory -->|reads/writes stock, read model| inventoryDb
inventory -->|"cache-aside: GET/SET/DEL"| broker
orders -->|"publish order_created"| broker
broker -->|subscribe| inventory
classDef container fill:#1168bd,stroke:#0b4884,color:#fff
class gw,orders,inventory,broker,inventoryDb container
Two things worth noticing before the patterns themselves: order_created’s subscriber now does
two jobs from one event (the fulfilment log from Part 2, and this round’s read model), and Redis
now does two unrelated jobs on the one instance (message broker, and cache). Neither pairing knows
the other exists.
CQRS: a read model built from an existing event
Database-per-service means a query needing data from more than one service’s database can’t be answered with a single join - there’s no shared database to join across. CQRS answers this by keeping the read side separate from the write side: a model built in advance, shaped for the question it needs to answer, rather than computed live from the write side’s own tables.
flowchart LR
writer["orders-service (Container)"]
writeDb[("orders.db (Container)")]
event[("order_created event (Container)")]
projector["inventory-service subscriber (Container)"]
readDb[("fulfilled_orders - read model (Container)")]
reader["GET /orders/summary (Container)"]
writer -->|command| writeDb
writer -->|publishes| event
event -->|consumed by| projector
projector -->|builds/updates| readDb
reader -->|query| readDb
classDef container fill:#1168bd,stroke:#0b4884,color:#fff
class writer,writeDb,event,projector,readDb,reader container
GET /orders/summary answers “what was stock after this order” - something that would otherwise
need a join across orders-service and inventory-service’s own, separate databases. Instead,
the existing order_created event subscriber (already writing a fulfilment/audit log since Part
2) takes a snapshot of inventory.stock at the moment it processes each order, and stores it
alongside the fulfilment record. No new event was published for this - the read model reuses what
was already there.
One honesty point worth stating plainly: stock_after is a snapshot taken when the event was
processed, not stock immediately after this specific order’s own reservation in isolation.
Another order’s reservation can land in between. Fine for a summary view; would matter for
anything needing an exact per-order figure.
Cache-Aside: caching the busiest read
GET /inventory is read far more often than stock actually changes - the UI polls it every five
seconds. Cache-Aside serves it from Redis most of the time, falling back to SQLite only on a
cache miss.
flowchart LR
reader["GET /inventory (Container)"]
cache[("Redis cache (Container)")]
store[("inventory.db (Container)")]
writer["reserve / release / reset (Container)"]
reader -->|"1: check"| cache
cache -->|"2: miss - read, then populate"| store
writer -->|"on write: invalidate"| cache
classDef container fill:#1168bd,stroke:#0b4884,color:#fff
class reader,cache,store,writer container
A read checks Redis first; on a miss, it reads SQLite and populates the cache (10-second TTL)
before returning. Any write to stock - reserve, release, or reset - deletes the cache key rather
than updating it, so the next read repopulates it fresh. The response carries an
X-Cache: HIT/MISS header, and the demo’s UI shows it next to the Inventory heading, so the
pattern is observable rather than taken on faith.
Redis already ran in this architecture as the event broker from Part 1 - this reuses the same instance for the cache rather than adding a second piece of infrastructure, a deliberate trade-off (see the pattern catalogue for the full reasoning): the two roles now share fate if Redis is ever unreachable, where separate infrastructure would let one keep working if the other didn’t.
See it for yourself
# Cache-Aside: miss, then hit, then miss again after a write
curl -i http://localhost:8090/inventory | grep -i x-cache # MISS
curl -i http://localhost:8090/inventory | grep -i x-cache # HIT
curl -X POST http://localhost:8090/orders -H 'Content-Type: application/json' -d '{"item":"widget","qty":1}'
curl -i http://localhost:8090/inventory | grep -i x-cache # MISS again
# CQRS: the read model, compared against the live value
curl http://localhost:8090/orders/summary
curl http://localhost:8090/inventory
Terms used on this page
See the glossary for plain-English definitions of CQRS, Cache-Aside, TTL, and the rest.
Catalogued
Both new patterns are documented in Architecture Governance Platform alongside the ones from Parts 1 and 2 - see the pattern catalogue for the public copy.
On Azure instead of Docker Compose
Part 3 on Azure covers the same read model and cache - and the one place the Azure variant can’t reuse what Compose reuses, since its event broker (Service Bus) isn’t a cache the way Redis is.