CQRS & Projection Lag Lab (Interactive)
Split write and read models, then race the async projection and price its staleness. Run commands through a write pipeline while an asynchronous projector rebuilds the read model; sweep read amplification and projection rate to expose lag, stale reads, and saturation.
CQRS: One DB vs Segregated Read Store
Flood a 50:1 read-heavy catalog and choose: torture the write model with joins, or pay projection lag for sub-5 ms reads.
Write Latency
12 ms
normalized, ACID only
Search/Feed Read
4 ms
pre-joined ES documents
Projection Lag
0 ms
backlog 0 · projected 0
Stale Reads
0
read-your-writes OK
Command → outbox → Kafka → projection worker → Elasticsearch/Redis
Uber answers 100+ million riders' receipt searches in 15 ms because projection workers pre-join ride, driver, and GPS data into Elasticsearch — the write path never pays for reads. CQRS's honest cost is eventual consistency: drop commands to 800/s throughput and watch lag; then try optimistic UI and the If-Match-Version token to hide or harden the 0 ms window.
How It Works Under the Hood
Object-relational impedance makes one schema serve both heavy writes and exotic reads, so CQRS segregates them: a normalized write model accepts commands and emits events; denormalized read models (projections) consume those events and rebuild query-optimized views. The catch is the arrow of time—projections lag, reads can be stale, and the UI must cope with optimistic rendering or version tokens. The reward is independent scaling: read-heavy dashboards hammer the replica without touching write capacity at all.
Core Architectural Principles
- Projection lag (ms) is (unprocessed events / projection rate); every read served behind it is potentially stale.
- Combined single-DB load saturates one shared capacity pool; segregated stores scale each axis independently.
- Mitigations trade correctness for perceived freshness: optimistic UI hides lag, version tokens refuse stale reads.
Introduce CQRS as a scaling answer, not a religion, and pair it with event sourcing only when the write and read shapes truly diverge. Always name the staleness contract: "search may be seconds behind; checkout reads the write model," and show you handle it with projection version tokens or client-side optimistic state.
Independent read/write scaling and bespoke views come with eventual consistency, projection failure handling, and a more complex codebase.