CQRS & Event Sourcing
CQRS (Command Query Responsibility Segregation) is the idea that the model you use to change data and the model you use to read it don't have to be the same model. Event sourcing is the idea that instead of storing current state, you store the sequence of events that produced it, and derive state by replaying them.
They're independent patterns that are constantly confused for one another. You can do CQRS with a single boring relational database. You can event-source without CQRS. They're paired so often because event sourcing makes reads awkward — replaying a thousand events to render a list page is absurd — which forces you to build separate read models, which is CQRS.
Both are powerful and both are frequently applied where a normalized table and an UPDATE statement would have done. This page covers how they work, and is honest about when to walk away.
TL;DR
- CQRS = separate write model from read model. It can be as light as different classes over one database, or as heavy as separate databases synced by events.
- Event sourcing = the event log is the source of truth; current state is a derived fold over that log.
- Together they give you: an audit trail by construction, temporal queries, read models shaped exactly for each screen, and independent read/write scaling.
- The price is eventual consistency (read models lag writes), event versioning forever, and a large jump in operational and cognitive complexity.
- Commands express intent and can be rejected; events are immutable facts in the past tense and cannot.
- Apply them to a single complex subdomain where audit and business-rule richness justify it — never as a system-wide default.
Quick Example
An event-sourced aggregate with a projection feeding the read side:
Core Concepts
Commands, events, and queries
Naming discipline matters more than it looks. UpdateOrderTable is not a command; CancelOrder is. The past-tense event name forces you to model what happened rather than what fields changed — which is the whole point, because "status went from A to B" loses the reason, and the reason is usually what the business cares about.
CQRS without event sourcing
The lightweight version, and the one most teams should start with:
No eventual consistency, no event store, no projections to rebuild — just an acknowledgment that the shape optimal for enforcing invariants is rarely the shape optimal for rendering a dashboard. Read models can be database views, materialized views refreshed on write, or a separate query service hitting read replicas.
Event sourcing: state as a fold
The log is the truth; every other representation is a cache you can throw away and rebuild. That single property is what buys you the audit trail, time travel ("what did this look like on March 3rd?" is a fold over events with ts <= '2026-03-03'), and the freedom to add a new read model six months later that's fully populated from history.
It also means you can never delete or edit an event. Corrections are new compensating events (OrderTotalCorrected), the same way accounting ledgers work.
Aggregates and the consistency boundary
An aggregate (from domain-driven design) is the unit of consistency: one aggregate, one stream, one transaction, all its invariants enforced together. Cross-aggregate consistency is eventual, coordinated by sagas.
Sizing aggregates is the hardest design call in this style. Too large — "the whole warehouse inventory" — and every concurrent write conflicts. Too small and you can't enforce the invariant you needed. Default to small, and challenge any invariant that appears to demand a large one; it's often a business rule that tolerates eventual detection and compensation.
Optimistic concurrency
Two writers loading version 5 and both appending must not both win:
The composite key means the second append at version 6 fails with a unique violation. The handler reloads, re-decides against the new state, and retries — and because the decision is pure, retrying is cheap and safe.
Projections and the Read Side
A projection consumes the event stream and maintains a query-optimized view. You can have as many as you have screens, each in whatever store fits: Postgres tables for filtered lists, Elasticsearch for search, Redis for counters, a warehouse for analytics.
Rules that keep projections manageable:
- Idempotent. Projections get replayed. Track the last processed position per projection and use upserts, not increments. See idempotency.
- Rebuildable. Deleting a read model and replaying from event 1 must reproduce it exactly. This is your escape hatch for every projection bug.
- No business decisions. Projections transform; they don't validate or reject. Logic that belongs in the write model must not leak here.
- Independently versioned. Rebuild a new version alongside the old, verify, then switch reads — never rebuild in place on a live system.
Snapshots
Folding 50,000 events on every command gets slow. Periodically persist the folded state:
Snapshots are a cache, never truth. Deleting every snapshot must leave the system correct, just slower. Take them every N events (500–1000 is typical), and treat a long stream as a signal that the aggregate might be modeled too coarsely.
Eventual consistency in the UI
This is where CQRS gets sold to users, and where teams underestimate the work. The write succeeded; the read model hasn't caught up; the user hits refresh and their order is missing.
💡 Tip: Projection lag is a first-class SLO. Measure it (
now() - last_processed_event.occurred_atper projection), alert on it, and display it in internal tools. "The dashboard is stale" is otherwise unfalsifiable.
Event Versioning
Events are immutable, and you'll be reading events written years ago. Schema evolution is the permanent tax of event sourcing.
The alternative — rewriting history so all events use the new shape — is possible (copy the stream, transform, swap) but destroys the audit guarantee that motivated event sourcing in the first place. Treat it as a last resort.
When to Use — and When Not To
Good fits
- Auditability is a hard requirement — finance, healthcare, legal, compliance. You'd be building an audit log anyway; event sourcing makes it the primary store rather than a parallel one that drifts.
- The domain is genuinely event-shaped — ledgers, order lifecycles, insurance claims, shipment tracking, workflow engines.
- "Why did this happen?" is a routine question — support and ops need to replay a customer's exact history.
- Wildly asymmetric read/write patterns — thousands of reads per write, each read wanting a different shape.
- Temporal queries matter — as-of reporting, retroactive corrections, what-if replays.
Bad fits
- CRUD — if
UPDATE users SET email = ?captures the whole business rule, you're adding an event store to model a text field. - Small teams and early products — the domain model will change weekly, and every change is an event-versioning exercise.
- "We might need it later" — you can retrofit CQRS on a bounded context later; a premature system-wide event store is very hard to unwind.
- Strong global consistency requirements — if no read can ever be stale anywhere, the pattern is fighting you.
The honest middle path
Most successful systems using these patterns apply them to one or two bounded contexts — the ledger, the order lifecycle — and keep everything else as boring CRUD over a relational database. That's not a compromise; that's the intended use. A "fully event-sourced system" is usually a warning sign.
Best Practices
Name events for business facts, not data changes
The second one is still useful in three years. The first one is a diff.
Keep command handlers pure
Load state, decide, return events. No I/O inside the decision. Pure handlers are trivially unit-testable — given these past events and this command, expect these new events — and safe to retry on a concurrency conflict.
Store rich metadata alongside every event
Correlation ID, causation ID, actor, timestamp, and schema version. Without correlation and causation IDs, debugging a chain of events across services is guesswork. See distributed tracing.
Write the event and its publication atomically
Appending to the event store and notifying subscribers must not be a dual write. Either the event store is the log that subscribers tail, or you publish via the transactional outbox.
Make replay a routine, tested operation
Rebuild a projection from scratch in CI against a realistic event volume. If a full rebuild takes eight hours and nobody has tried it in a year, you don't actually have the recovery story you think you have.
Keep PII out of an immutable log
You cannot delete an event, and GDPR erasure requests are not optional. The standard approach is crypto-shredding: store personal data encrypted with a per-subject key held outside the log, and delete the key on request — the events remain, the data is unrecoverable.
Common Mistakes
Treating CQRS and event sourcing as one decision
Events that carry a diff instead of a fact
Doing I/O inside the aggregate
Querying the event store to answer queries
Mutating history to fix a bug
Pretending the read model is consistent
FAQ
Do I need event sourcing to do CQRS?
No, and conflating them is the most common mistake in this space. CQRS is just separating read and write models — you can do it with two sets of classes over one Postgres database and gain most of the clarity benefit at almost no operational cost.
How is event sourcing different from an audit log?
An audit log is written alongside state and can drift from it — a bug that skips the log leaves you with an incomplete history you can't detect. In event sourcing the log is the state; there is nothing to drift from, because current state is derived from it.
Isn't replaying thousands of events too slow?
For a single aggregate, folding a few hundred events is microseconds. Long streams get snapshots. The slow operation is rebuilding a projection across all streams, which is a background job you run rarely and can parallelize by stream.
How do I delete user data under GDPR?
Crypto-shredding: encrypt personal fields with a per-subject key stored in a mutable keystore, and delete the key on an erasure request. The immutable events remain structurally intact; the personal data becomes unrecoverable. Plan this before your first production event, not after the first request.
What database should I use for the event store?
Plain PostgreSQL handles event sourcing well — an append-only table with PRIMARY KEY (stream_id, version) gives you ordering and optimistic concurrency for free, and JSONB stores the payloads. Purpose-built stores (EventStoreDB, Marten, Axon) add subscriptions, projections, and stream management. Start with Postgres.
Can I add CQRS to an existing system?
Yes, incrementally, and that's the right way. Pick one bounded context, add a read model fed by change data capture or domain events, move its queries over, and evaluate. This gives you most of CQRS's benefit without rewriting the write side.
Related Topics
- Domain-Driven Design — aggregates, bounded contexts, ubiquitous language
- Event-Driven Architecture — the broader style CQRS lives inside
- Sagas & Distributed Transactions — coordinating across aggregates
- Change Data Capture — a low-cost path to read models without event sourcing
- Microservices — where CQRS's per-service data ownership fits
- Clean Architecture — keeping the domain free of infrastructure
- Idempotency — required for replayable projections
- Stream Processing — projections at scale
- Design Patterns — the pattern vocabulary this builds on
- System Design — evaluating the tradeoff in context