Event sourcing and CQRS
covers event store as source of truth · optimistic concurrency (expected version) · replay & snapshots · projections & read models · CQRS with and without event sourcing · event schema versioning & upcasting
Why current state is not stored
A CRUD service stores the latest row and overwrites it on every change. An event-sourced service stores the change itself. Each entity has its own stream, an ordered, append-only sequence of events (OrderPlaced, ItemAdded, OrderCancelled), and that stream is the system of record. There is no current-state row to read. You get the state by replaying the events over an empty object, folding each one in until you reach the end.
So a command handler always does the same dance: read the entity's events, rebuild its state in memory, run the business rule against that state, and append the new events. Nothing is updated in place. That is the trade you are making: writes become cheap append-only inserts with no row locks, and in exchange every read of current state costs a replay (snapshots, in the senior notes, cut that cost).
Wrong "Event sourcing just means I also write an event to an audit log after I update my table." That is a dual write with two sources of truth that drift apart. In event sourcing the events are the only truth; the table, if you keep one, is derived from them.
Optimistic concurrency with an expected version
Because state comes from replaying a stream, two handlers can load the same stream, both decide the same action is allowed, and both try to append. The event store stops the collision with optimistic concurrency: when you append, you pass the version you replayed from, and the store appends only if the stream is still at that version.
The comparison (from EventStoreDB / Kurrent) is exactly: expected equals current, the events are appended and the stream advances; expected is lower than current, the append is rejected with a WrongExpectedVersion conflict because someone wrote in between. On a conflict you reload the now-longer stream, re-check your rule against the new state, and retry or give up.
append("seat-availability-42", [SeatReserved], expectedVersion = 17)
# current == 17 -> appended, stream now at 18
# current == 18 -> WrongExpectedVersion, reload and retry
Wrong "Passing expected-version Any is the safe default." Any turns the check off, so both writes land and you double-book. Pass the real version whenever an invariant depends on the state you just read.
Read models are projections, and they lag
Replaying a stream answers "what is the state of this one order." It cannot answer "all orders for this customer" without reading every stream, which does not scale. So you build a read model: a separate, query-shaped store (a SQL table, a document, a search index) kept up to date by a projection that subscribes to the events and applies each one.
The projection runs after the append, catching up asynchronously, so the read side is eventually consistent with the write side. Right after a command succeeds, a query can still return the old value for a short window. Your UI has to expect that: show the user their own change optimistically, or tell them a refresh may be needed, rather than assuming a write is instantly visible everywhere.
Because the events are the source of truth, a read model is disposable. If it is wrong, or you want a new shape of it, you drop it and rebuild by replaying the event store from the start. That rebuildability is one of the real payoffs of sourcing events.
CQRS with and without event sourcing
CQRS (Command Query Responsibility Segregation) is the smaller, separate idea that reads and writes get different models. The write model has the validation and business rules; the read model returns plain query-shaped data with no domain logic. Fowler's framing: use a different model to update information than the model you read it with.
These two patterns are often taught together but neither requires the other:
| You have | What it means |
|---|---|
| CQRS, no event sourcing | Two models over ordinary storage: writes go through a rich domain model, reads hit a query-optimized view or replica. State is still stored, not replayed. |
| Event sourcing, no CQRS | You persist events and replay a single stream for state. Fine when you only ever read one entity at a time and never need cross-entity queries. |
| Both together | The event store is the write model and the source of truth; projections build the read models. This is the common pairing, and where most of the complexity lives. |
Wrong "CQRS means two databases." The basic form of CQRS is two models over one shared database; separate stores are the advanced version, and they are what forces you to keep the read store in sync through events. Fowler is blunt that CQRS adds risky complexity and belongs on specific parts of a system, not the whole thing.
Replay cost and snapshots
Rehydrating an entity means replaying its whole stream, so cost grows with stream length. A long-lived aggregate (an account open for years, a busy inventory item) can accumulate tens of thousands of events, and replaying them on every command gets slow. A snapshot is a serialized copy of the entity's state at a known version; to rehydrate you load the latest snapshot and replay only the events after it.
Two things to keep straight. A snapshot is an optimization, never a second source of truth: the stream stays authoritative, and you can throw every snapshot away and regenerate them by replay. And snapshots do not fix an unbounded stream by themselves. The deeper fix is temporal modeling, closing a stream and starting a new one when the entity's lifecycle rolls over (a new billing period, a new order), so no single stream grows forever. Snapshot cadence is a tuning knob: every N events trades snapshot storage against replay time.
Evolving event schemas without rewriting history
The event store is permanent and immutable, which collides with the fact that event shapes change. You cannot go back and edit old events, so the read path has to cope with every version that was ever written. Serializing a domain class straight to JSON is the trap here: it welds years of stored history to today's class definition, and a rename breaks deserialization of everything old.
The tools, used alone or in combination:
- Tolerant reader. Consumers ignore unknown fields and default missing ones, which absorbs additive, non-breaking changes (a new optional field) with no transformation of stored events.
- Versioned event types. Put a version in the event (type name or envelope metadata) so a consumer can branch on it.
- Upcasting. Register transform functions that convert an old event shape to the current one during deserialization, chained so the domain code only ever sees the latest version. The stored bytes never change, so immutability holds.
- In-place migration (rewriting stored events) breaks immutability and the audit trail. Treat it as a last resort rather than routine practice.
A business mistake is a separate axis from a schema change. You correct a wrong fact with a compensating event (SeatReservationCancelled), which leaves the original event in place and records that it was reversed. That reversal history is itself information a state-only store would have thrown away.
Wrong "We fixed the bug in the code, so the bad events are fixed." The code that produced them changed; the events already written are still wrong and will replay wrong. You need compensating events or an upcaster to neutralize them on read.
Idempotent projections and eventual consistency
Delivery from the event store or its log to a projection is typically at-least-once, so a consumer can see the same event twice (a redelivery after a crash, a retry after a slow ack). A projection that blindly applies each event will double-count: two decrements for one reservation, two rows for one order. The projection has to be idempotent, either by tracking the last processed event number per stream and skipping anything at or below it, or by making the state change naturally repeat-safe (a set-to-value rather than an increment).
The lag between append and projection is the eventual-consistency window, and it produces real anomalies you design around. A user completes a write and immediately queries a read model that has not caught up. Two entities interact across their separate streams (stock runs out while an order is being placed) and optimistic concurrency on a single stream cannot catch a conflict that spans two. Microsoft's guidance is explicit that you reconcile these at the application level, for example by back-ordering or advising the customer. The read is genuinely stale in that window, and the application design has to account for it.
When event sourcing is the wrong tool
Microsoft calls event sourcing a complex pattern with significant trade-offs and says that for most systems and most parts of a system, traditional data management is enough. The honest default is to not event-source, and to reach for it only where the ledger earns its cost: a payment ledger, an order pipeline, anything where audit, replay, and temporal queries are real requirements. It does not have to be all-or-nothing; apply it to the one subsystem that needs it and keep CRUD elsewhere.
The costs are concrete, and each is a place a design gets it wrong:
- No ad-hoc query over current state. The only native read is "give me one entity's stream," so every list or search needs a projection you build and operate.
- Schema evolution is a permanent tax: tolerant readers, versioning, upcasters, forever.
- Eventual consistency and at-least-once delivery push idempotency and stale-read handling into every consumer.
- Personal data fights an append-only store. Right-to-be-forgotten cannot delete an event without breaking the stream, so keep PII outside the store referenced by id, or crypto-shred by encrypting per subject and destroying the key.
- A message broker is not an event store. A Kafka topic distributes events well but lacks per-entity stream reads and expected-version appends, so using it as the system of record means rebuilding those guarantees yourself.
Wrong "CRUD couldn't give us an audit trail, so we event-sourced the whole system." An append-only audit log or change-data-capture gives you history without turning every read into a replay and every schema change into an upcasting problem. Event sourcing is worth it when you need to make decisions by replaying past events, not merely to keep a record that they happened.
dig deeper
Primary sources behind these notes - the specs and official docs worth reading in full.
gapmap pro
You've read the Event sourcing and CQRS notes. An interviewer will ask you to prove them.
Know you're ready. Don't hope.
The notes are free: every topic, senior depth. Pro is what turns reading into readiness you can prove:
Mock interviews on your topics
An AI interviewer that pushes back with follow-ups and grades you honestly. Per module, fair-use unlimited.
Voice test interviews
The dress rehearsal, out loud: questions from your map, a transcript, and a per-answer verdict.
A plan to your date
Ready or not ready, per module, as the interview approaches. Recomposed whenever your goal changes.
A mentor inside every lesson
Stuck on the senior notes? Ask, drill deeper, get re-quizzed on the spot.
Pro $20/mo · Pro+ $50/mo · every study note on the site stays free