Event sourcing's obvious problem — replaying an entity's entire history just to answer "what's its current status" — has a standard answer, and that answer is CQRS: stop using the same model to handle both writes and reads in the first place.
3 min read
A normal application uses one model for everything: the same Order table (or class) handles both "record that this order was placed" (a write) and "show me all shipped orders from this week" (a read). Event sourcing makes this collide head-on — the natural way to write is appending an event to a log, but the natural way to read is a queryable, indexable table, and a log is a terrible thing to query directly (finding "all shipped orders" means replaying every event for every order, every time).
CQRS (Command Query Responsibility Segregation) uses two separate models instead of one: a write model that accepts commands and validates/persists them (often as events, though CQRS doesn't strictly require event sourcing), and a separate read model — one or more denormalized, query-optimized views, updated asynchronously from the write side, that's all a query ever touches.
Each read model is shaped for exactly the queries it needs to answer — one table might be indexed by customer, another pre-aggregated by week and status — instead of forcing every different access pattern through one general-purpose schema that's well-suited to none of them.
If the write side is event-sourced (lesson 5), updating a read model is exactly what a consumer subscribed to those events does — consume each OrderCreated/Shipped/etc. event and update the relevant read table accordingly, the same event-consumption pattern this whole domain has been building. But CQRS is a separate idea: a system can split its write and read models even with a traditional database behind the write side, using change-data-capture or explicit application-level events to keep read models updated — event sourcing makes the event stream driving the read models free (it's the source of truth anyway), while a non-event-sourced system has to deliberately emit those events as a side effect of each write.
Because read models update asynchronously from events, there's a real window — usually milliseconds, sometimes longer under load — where a write has succeeded but a read model hasn't caught up yet. A user who places an order and immediately refreshes "my orders" can briefly see a list that doesn't include it yet. This is a genuine trade-off, not a bug to eliminate: CQRS buys read-side performance and flexibility at the cost of strict, always-up-to-date consistency between write and read — acceptable for most real use cases, and a real problem for any flow that genuinely needs to read back its own just-written data synchronously (which should usually just query the write model directly instead).
A CRUD app with one reasonable read pattern per entity gains nothing from splitting models that would otherwise be identical — the added asynchronous-update machinery is pure complexity with no corresponding benefit. CQRS earns its cost specifically when read and write patterns genuinely diverge: high write volume with very different, numerous read shapes, or a read side that needs to scale independently of writes.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does CQRS split into two separate models?
2. Why does CQRS pair naturally with event sourcing, even though it doesn't strictly require it?
3. What real cost does CQRS introduce on the read side?
Message Queues & Event-Driven Architecture