CQRS and event sourcing are powerful and expensive — a decision framework for when the complexity genuinely pays off, and what to do when it doesn't.
Command-query separation is easy to love. Commands mutate the write model, queries read from a dedicated read model, and every state change is an event appended to a permanent history. It feels like the “correct” architecture for any serious system. The problem is that CQRS and event sourcing are not optimisations — they are trades, and the price tag only shows up after you’ve shipped. This post is the decision framework I wish I’d had: what you’re actually buying, what it genuinely costs, the signals that say yes, and the alternatives that are usually better.
The split pays for itself in exactly four ways:
These are real. They are also the only things CQRS buys you — everything else people attribute to it (decoupling, scalability, event-driven fan-out) is achievable more cheaply by other means.
The costs are the part most write-ups skim. In production, they land like this:
Eventual consistency is a product decision, not a technical one. The read model lags the write model. For a trading application, “your position summary is up to 200ms stale” is usually fine; “the order you just placed isn’t visible yet” is not. Every consumer of the read model has to tolerate lag, and somebody has to own that conversation with the business.
Events are contracts that outlive their authors. Once OrderPlaced is in the store, its schema is effectively permanent — you can add fields with defaults, but you can never change the meaning of an old event. Every schema evolution is a compatibility exercise across every projection, forever.
Replay is a second system to operate. Snapshots, projection rebuilds, idempotent consumers, offset management — the machinery that makes history useful is a permanent operational surface, not a one-off migration.
Debugging gets harder, not easier. An aggregate’s state is a fold over thousands of events. A stack trace tells you what method failed, but the why now requires walking the event history — which is exactly why the audit benefit and the debugging cost are two sides of the same coin.
Adopt CQRS with event sourcing when the history is the product:
Most systems fail at least one of those signals, and the alternatives are cheaper and honest:
Where it genuinely pays off, it pays off visibly. Consider an order lifecycle on a betting exchange:
PlaceOrder (command)
-> Order aggregate validates stake, odds, exposure
-> OrderPlaced event appended to the store
-> Projections rebuild: trader's ladder, risk dashboard, P&L
SettleMarket (command)
-> Settlement event appended
-> Replay settles every affected position idempotently
The write model is a small, fast aggregate. The read side is several projections, each shaped for its screen. And when a dispute arrives — “my bet should have won” — the answer is not a grep through logs but a replay of that order’s events, which is exactly the property you cannot get from a mutable orders table. The /cqrs/ visualiser animates this exact flow: commands mutate the write model, events propagate through the bus, and projections sync with visible eventual-consistency lag.
You don’t have to jump to the full architecture to get most of the benefit:
CQRS with event sourcing is a commitment, not an upgrade. It pays off when the history is the product — auditing, replay, many projections from one immutable stream. It is a tax when you adopt it for scalability you don’t have, or decoupling you could get from an outbox. Ask which of the four benefits your system genuinely needs; if the answer is “audit and reconstruction, always”, it’s worth it. If it’s “we want to be modern”, it isn’t.
If you’re weighing CQRS and event sourcing for a new system and want an honest read on whether the complexity buys you anything, let’s talk.