Available Hire Me
← All Writing Architecture

CQRS and Event Sourcing in Practice — When the Complexity Pays Off

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.

What you’re actually buying

The split pays for itself in exactly four ways:

  • Independent read models. The same facts drive a trading ladder, a risk dashboard and a P&L report. CQRS lets each projection be shaped for its consumer instead of one compromised model serving all three.
  • A permanent, immutable history. The event store is the source of truth; current state is just a fold over it. Audit, dispute resolution and “what did the system believe on date X” become free.
  • Replay and reproduction. Rebuild any aggregate from its events, or replay the whole stream into a fresh projection. When a bug only shows up after 10,000 orders, you can reconstruct the exact sequence that caused it.
  • Contention relief. Writes stop competing with heavy analytical reads on the same model and the same locks.

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 real cost inventory

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.

Signals that say yes

Adopt CQRS with event sourcing when the history is the product:

  • Audit and reconstruction are first-class requirements. Betting exchanges are the canonical example: every order, price tick and settlement is immutable evidence. If regulators, disputes or “what happened at 14:03:02” are daily questions, the event store is your answer.
  • One stream, many projections. The same facts must feed dashboards, reports, alerts and a public API. Each is a separate read model over one history.
  • Reproducibility matters. You need to rebuild state as of an arbitrary date — reproducing a market’s state for a backtest or an investigation.
  • Write contention is measured, not assumed. You have a concrete command-throughput problem that a split demonstrably relieves.

Signals that say no — and what to use instead

Most systems fail at least one of those signals, and the alternatives are cheaper and honest:

  • CRUD with a few queries, moderate load — use a normal aggregate, a read repository, and maybe a denormalised table or two. CQRS adds a second model to keep in sync for no measurable benefit.
  • You just need other services to react — that’s event-driven integration, not event sourcing. A transactional outbox plus Kafka gives you reliable events without making your state immutable history. I covered the pattern in transactional outbox with Spring Boot and Kafka.
  • Multi-service rollback is the worry — that’s a saga, not a reason for CQRS. See the saga pattern with Spring Boot and Kafka.
  • Read performance is the pain — fix the query, add an index, or add a materialised view. “Read model” is a concept you can adopt without adopting CQRS wholesale.

A worked example: the betting exchange order lifecycle

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.

The minimum viable adoption path

You don’t have to jump to the full architecture to get most of the benefit:

  1. Start with domain events, not CQRS. Model the domain’s events first (an Event Storming session is the fastest way — see event storming to Kafka topic design). Events are useful whether or not you split models.
  2. Keep one write model. Add a projection only when a second consumer of the same facts actually appears.
  3. Snapshot long streams. Once an aggregate passes a few hundred events, reconstructing it from scratch on every command gets slow — snapshot and replay from the snapshot.
  4. Adopt the read model when it pays. The moment a query shape starts fighting the write shape, split — you already have the events to build it from.

The honest summary

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.

Samuel Jackson

Samuel Jackson

Senior Java Back End Developer & Contractor

Senior Java Back End Developer — Betfair Exchange API specialist, Spring Boot, AWS, and event-driven architecture. 25+ years delivering high-performance systems across betting, finance, energy, retail, and government. Available for Java contracting.