In an architecture that combines Event Sourcing with CQRS (Command Query Responsibility Segregation), a client submits a command to change data. Walk through what happens on the write side before that change becomes visible to a query.
answer
- load-replay-validate-append
- aggregate state is derived from replay, not stored
- command side never writes the read model directly
- optimistic concurrency via expected stream version
- publish then project asynchronously
basics
~10 sThe command side loads the past events for that thing, checks the business rules, creates one new event describing what happened, and appends it. Nothing is overwritten, only added.
solid answer
~30 sA command handler loads the target aggregate by replaying (or restoring from a snapshot plus replaying) its historical events to get current state, validates the command against business rules, and if valid, appends exactly one new event to that aggregate's stream using an optimistic-concurrency check against the expected stream version. The event store is the append target, not the read database. Once appended, the event is published so downstream projectors can update read-model tables asynchronously. The command handler never writes to the query-side store directly, which is why the two sides can be scaled, modeled, and even stored independently.
go deeper
Can describe the load-validate-append flow at a high level, ideally with an analogy like a ledger.
Can name optimistic concurrency and stream versioning, and explain why snapshots help replay performance.
Can discuss the transactional boundary (one aggregate equals one append), idempotent event publishing, and the trade-offs against plain CRUD.
Can argue when Event Sourcing and CQRS are worth adopting at all versus simpler approaches, and the organizational cost of owning event schemas and projections long-term.
## What each half owns **Event Sourcing** stores state as an append-only sequence of immutable domain events rather than as mutable rows; the current state of an entity (an 'aggregate' in domain-driven design terms) is derived, not stored directly, by replaying its events in order. **CQRS** separates the model used to change state (commands) from the model used to read state (queries). Combined, the command side owns an aggregate and its event stream, while the query side owns one or more read-model projections built from those events. ## The flow, step by step Concretely, the flow is: - **(1)** a command arrives at a command handler naming a target aggregate by id; - **(2)** the handler loads that aggregate by fetching its event stream from the event store and replaying every event through the aggregate's apply logic to rebuild current in-memory state (for long streams this is sped up with a periodic snapshot, so replay only covers events since the last snapshot); - **(3)** the handler runs domain validation against that reconstructed state -- for example, refusing to ship an order that was already cancelled; - **(4)** if valid, it produces one new event (e.g. `OrderShipped`) and appends it to the stream, passing the version number it expected the stream to be at; - **(5)** the store either accepts the append, giving the stream a new version, or rejects it if another command changed the aggregate first (optimistic concurrency), forcing the handler to retry against fresh state; - **(6)** once appended, the event is durable and becomes the new source of truth; - **(7)** separately and asynchronously, one or more projectors subscribed to the event stream or a downstream bus consume the new event and update whatever read-model tables or search indexes serve queries. ## Why it exists This exists because a single mutable row model forces every reader and writer through one shape and one store, coupling scaling, schema, and technology choices that often have very different needs: writes need strong consistency and small, fast transactions per aggregate, while reads often need denormalized, search-optimized, or aggregated shapes serving far higher volume. Event Sourcing additionally gives a **full audit trail for free** (every state change is a permanent, inspectable fact) and lets you rebuild any number of new read models later by replaying history, since nothing was ever discarded on write. ## The trade-offs The trade-offs are real. On the plus side: - strong auditability; - the ability to add new projections after the fact by replaying history; - and a write path that is fast and uncontended because it only ever appends to one stream per command. On the minus side: - the read side is only eventually consistent with the write side (a projector takes some non-zero time to catch up); - the team must design and maintain event schemas that will live forever (since old events can never be silently rewritten); - and debugging requires reasoning about a system that has more moving parts than a single CRUD database. Complexity is the toll paid for those benefits, and teams that adopt Event Sourcing for entities with no auditing or historical-replay need often find the toll isn't worth it. ## Failure modes Failure modes show up mainly around concurrency and durability. - **Two commands race against the same aggregate.** The second one to attempt its append fails the optimistic-concurrency check and must retry -- if a handler doesn't implement retry, users see spurious command failures under load. - **The event store or the publish step fails** between appending and notifying projectors. The write is still durable (it's in the store) but projectors may lag until they catch up on their next poll or subscription resume; this is normal, not data loss, as long as projectors are built to resume from a durable checkpoint rather than relying on a live-only push. - **A snapshot mechanism is buggy.** Aggregate reconstruction can be slow (no snapshot, full replay every time) or, worse, wrong (a stale or corrupted snapshot silently producing incorrect state). ## Where it shows up A concrete, widely used real-world shape of this pattern is a banking ledger or an order-management system built with a framework such as **Axon Framework** or backed by a store such as **EventStoreDB**, where account balance or order status is never stored as a column to be updated in place; instead, every deposit, withdrawal, or status change is appended as its own event, and the balance or status shown to a user is a read-model column maintained by a projector that has consumed that account's or order's full event history.
- What stops two concurrent commands against the same aggregate from producing conflicting events?The append call passes the version the handler expected the stream to be at. If another command already appended an event and moved the stream to a newer version, the store rejects the second append as a conflict. The handler then reloads the current state and either retries the command or surfaces a conflict to the caller.
- Why not just update the read model synchronously inside the same operation as the event append?Doing so would couple the write path's latency and availability to every read model's store, defeating the point of separating them, and it often isn't even possible when the read model lives in a different storage technology such as a search index. Systems that truly need read-your-writes for a specific flow usually solve it by returning the updated data directly in the command's response, or reading straight from the just-updated aggregate, rather than making every projection synchronous.
Like a bank passbook: instead of erasing and rewriting your balance, each transaction is written as a new line in the book, and your current balance is just the sum of every line so far.
saying these in an interview costs you the question
- describes commands as directly updating the read model or database row
- thinks the aggregate's state is stored rather than derived by replaying events
- no mention of any concurrency or version check on append
- conflates the event store with a plain queue that has no durable, replayable log
- assumes read-side updates happen synchronously with the write