CQRS Pattern — Projection Lag and Stale Read Pitfalls
A 300ms projection lag caused duplicate payments and chargebacks.
- CQRS separates write models (commands) from read models (queries) for independent optimisation
- Commands change state using normalised stores with full business logic
- Queries read from denormalised, pre-joined views optimised for display
- Performance benefit: query latency drops ~70% because joins are eliminated at read time
- Production gotcha: eventual consistency means stale reads until projection catches up
- Biggest mistake: applying CQRS to simple CRUD — the complexity tax outweighs the gain
Imagine you have a library with one desk for checking out books (write) and separate, faster desks just for looking up books (read). The checkout desk has all the rules — you need a card, books must be returned — but the lookup desks are lean, with pre-sorted shelves so you find any book instantly. CQRS is like having these two different desks instead of one that tries to do both.
The Core Pattern
CQRS splits your system into two logical halves: the command side (write) and the query side (read). The command side receives commands—imperative instructions like 'PlaceOrder' or 'UpdateProfile'—validates business rules, writes to a normalised store, and publishes an event. The query side subscribes to those events and builds denormalised read models that serve UI or API responses without joins. This separation lets you scale read and write independently: you might have 10 read replicas and 1 write master, or use different database technologies entirely (e.g., PostgreSQL for writes, Elasticsearch for reads).
Maintaining the Read Model
Read models are not kept in sync by the write side. Instead, they are built and updated by projections—event handlers that listen to events and denormalise data into query-optimised tables. In the example below, an OrderProjection class listens for OrderCreated events and upserts a row in user_orders_view that includes denormalised user info and pre-joined item names. This removes all joins from the read path, making queries extremely fast. The trade-off is eventual consistency: there is a window between the write commit and the projection update where the read model is stale.
When to Apply CQRS (and When to Avoid It)
CQRS adds significant complexity: you now maintain two models, an event pipeline, and deal with eventual consistency. Apply it only when the benefits clearly outweigh the cost. The sweet spot is when read and write workloads have fundamentally different performance characteristics—for example, writes are transactional with frequent updates to many related tables, while reads need to aggregate data from multiple sources and serve high traffic with low latency. Avoid CQRS for simple CRUD apps where a single normalised model can handle both tasks efficiently. Start with a monolithic model, measure, and extract read models only when you hit a measurable performance bottleneck.
CQRS and Event Sourcing: Separate Patterns That Work Well Together
CQRS and Event Sourcing are often mentioned together but are independent. CQRS separates read and write models. Event Sourcing stores all state changes as an ordered sequence of immutable events, instead of current state. They combine naturally: the event store becomes the write model, and the projections build read models from those events. However, you can absolutely use CQRS without Event Sourcing — you can implement a simple write model that updates a normalised table and emits events to update the read model. Conversely, you can use Event Sourcing without CQRS by building a single model from events for both reads and writes (though that's unusual). The key insight: CQRS is about separation of concerns, not about storage strategy.
- CQRS separates the act of writing (commands) from the act of reading (queries).
- Event Sourcing stores every state change as an event — you replay events to get current state.
- You can have CQRS without Event Sourcing: just update a normalised write table and publish events for projections.
- You can have Event Sourcing without CQRS: though rare, you can read from the event stream directly.
- Together: Event Sourcing provides the event stream that CQRS projections consume.
Production Trade-offs: Consistency, Complexity, and Cost
Deploying CQRS in production introduces three core trade-offs you must design for. First, eventual consistency: your read model lags behind the write model. You must decide acceptable staleness per use case — 1 second for dashboards, near-zero for payment confirmations. Second, operational complexity: you now have two databases, an event bus, projections, and monitoring. Each component becomes a failure domain. Third, cost: storing two copies of data (write model + read model) doubles storage. Denormalised read models can be larger due to redundant data. You also pay for the event bus infrastructure. However, read query performance improvements can offset these costs by reducing need for read replicas and expensive joins.
Stale Read Model Leads to Customer Chargeback Spike
- Eventual consistency has a measurable latency bound — quantify it under peak load, don't assume 'eventual' means 'fast enough'.
- Always add idempotency in the write side to handle duplicate commands from user retries.
- Use read-after-write consistency for critical paths (payment confirmations, balance checks).
Key takeaways
Common mistakes to avoid
4 patternsNot handling projection failures
Assuming eventual consistency is negligible
Using the same database for both write and read models
Not designing commands to be idempotent
Interview Questions on This Topic
What is CQRS and what problem does it solve?
Frequently Asked Questions
That's Architecture. Mark it forged?
3 min read · try the examples if you haven't