Most applications read and write the same tables through the same model. That works until the shapes diverge: writes need strict business rules on one order at a time, while reads want lists, searches and totals across thousands of orders. CQRS separates the two, and event sourcing changes what you store in the first place.
They are often presented as a pair, but they are separate decisions. This post explains each one, how they fit together, what the diagram of a combined system looks like, and the costs that make many teams regret adopting them too early.
CQRS in one paragraph
Command Query Responsibility Segregation means using one model to change data and a different model to read it. Commands (PlaceOrder, CancelOrder) go to a write side that enforces the rules. Queries go to a read side shaped for the screens that need them. Martin Fowler's CQRS article, which credits the pattern to Greg Young, also carries the warning worth repeating: for most systems CQRS adds risky complexity. Use it where reads and writes truly diverge, not across a whole application.
CQRS does not require two databases. A write model and a set of SQL views over the same tables is already CQRS. Separate stores come later, when read load or read shapes demand them.
Event sourcing in one paragraph
Event sourcing stores the changes instead of the current state. An order is not a row with a status column; it is a stream of events: OrderPlaced, PaymentAuthorized, OrderShipped. The current state is whatever you get by replaying them. Fowler's event sourcing article describes the idea in detail. You get a full audit history for free, and you can build new views of the past at any time, because nothing was overwritten.
How they fit together
Event sourcing almost forces CQRS. Replaying events is a fine way to load one order before changing it, but a terrible way to answer "show me all open orders over 100 dollars". So the write side works with event streams, and the read side keeps separate read models that are updated from those events.
The flow:
- A command arrives at the command side.
- The handler loads the aggregate (the order) by replaying its stream, checks the business rules and decides which new events to emit.
- It appends the events to the event store with the expected version of the stream. If another command appended in the meantime, the append fails and the command retries. This is optimistic concurrency, and it replaces row locks.
- Projectors subscribe to the event stream and update read models: a table for the order list, a search index, counters for a dashboard.
- Queries read only from read models.
The event store
An event store needs few features, but it needs them exactly:
- Append-only streams, one per aggregate, with a version number per event.
- Conditional append on the expected version, atomically.
- Reading a stream from the start or from a version.
- Subscriptions to all new events in order, with a position that a projector can store and resume from.
Dedicated event stores provide this, and so does a relational table with a unique constraint on stream ID and version, plus an outbox or change feed for subscriptions.
Read models and eventual consistency
Projections run after the event is stored, so read models lag behind by milliseconds or, during a backlog, much longer. Design for it:
- After a command, return the new version and let the client wait until the read model has caught up, or show the result from the command response.
- Give each projector its own position, so a slow search index does not delay the order list.
- Make projections idempotent. Projectors restart and re-read events; applying OrderShipped twice must not count two shipments.
The payoff is that read models are disposable. A new screen needs a new shape: write a projector and replay history into it. A projection bug: fix it, drop the read model and rebuild.
A worked example: a new report
A month after launch, finance asks for revenue per product category per day, including history. In a CRUD system you would write a migration and a backfill script, and history before the change might be gone. With event sourcing, you write a projector that listens to OrderPlaced and OrderRefunded, point it at the start of the event log and let it replay. When it catches up, the report covers every order since launch, and it keeps itself current from then on.
The architecture diagram
Diagram contents
- User
- Command side
- Event store
- Event stream
- Projectors
- Read model: order list
- Read model: search index
- Read model: daily totals
- Query side
- PlaceOrder
- append
- subscription
- GET /orders
How to read it:
- Top row, the write path: the user's PlaceOrder command goes to the command side, which appends events to the event store. A subscription feeds the event stream.
- Right: projectors consume the stream and keep three read models up to date, each in the storage that suits its queries.
- Bottom left, the read path: queries go to the query side, which only reads read models and contains no business rules.
- The Markdown block shows one stream with its versions, which makes the expected-version check easy to explain.
The costs
- Schema evolution never ends. Events are stored forever, so old event versions must stay readable. Plan for upcasting (converting old events to the current shape on read) from day one.
- Deleting data is hard. Regulations that require erasure conflict with immutable logs; keep personal data out of events, or encrypt it per person and delete the key.
- Long streams get slow to load. Snapshots of an aggregate's state every few hundred events fix it.
- Debugging moves. Bugs show up as wrong read models, and the fix is a replay, which needs tooling.
- Team skills. Event modeling is a different way of thinking. One event-sourced core domain is plenty; the rest of the system can stay CRUD.
What to put on the diagram
- Two clearly separate paths for commands and queries.
- The event store as the source of truth, and read models drawn as derived.
- Each projector and its read model, so readers see which views can lag.
- An example stream, with versions, to anchor the discussion.
The event and consumer mechanics are covered in the event-driven architecture post, and the transactional outbox pattern explains how to publish events reliably when you are not using a dedicated event store. The Kafka architecture diagram covers the stream itself.
Build it in Archboard
- Sign in with Google or GitHub and create a diagram.
- Add a Person and Container nodes from the C4 Model pack for the command side, the projectors and the query side.
- Add a Database for the event store and Database nodes for the read models, plus Kafka and Redis from the Data pack for the event stream and the counters.
- Draw the write path on top and the read path below, with projectors feeding the read models from the right.
- Add a Markdown block from the Core pack with an example event stream.
The example uses 10 diagram nodes, within the Free plan.
FAQ
Do CQRS and event sourcing have to be used together? No. CQRS works with a normal database, and event sourcing can feed a single read model. Event sourcing without some form of CQRS is rare, because queries over replayed streams are slow.
When should I not use CQRS? When reads and writes use the same shape of data, which is true for most CRUD screens. Apply it to the parts of the system where they diverge.
How do read models stay consistent with the event store? Eventually. Projectors process events in order and record their position; after a failure they resume from it. Clients must tolerate a short delay.
How do I change an event's structure later? Version events and convert old versions when reading them (upcasting). Never edit stored events in place.
What is a snapshot in event sourcing? A saved copy of an aggregate's state at a given version, so loading it means reading the snapshot plus the events after it instead of the whole stream.