CQRS and Event Sourcing
Definition
CQRS uses one model to change data and a different model to read it. Event sourcing stores every change as an event and rebuilds the current state by replaying them.
Why it exists
One model for both reads and writes forces compromises, since queries want convenient shapes and writes want strict rules. Splitting them lets each be optimised, and events keep a full history.
How it works
CQRS: Command Query Responsibility Segregation
Use different models for changing state (commands) and reading state (queries).
Why separate them?
- Reads and writes have different shapes. Writes need normalised, invariant-protecting aggregates; reads want flat, join-free, pre-computed views.
- They scale differently (reads usually dominate).
- Each side can use the best store: relational for writes, Elasticsearch or Redis for reads.
- It solves cross-service queries: build a read model by consuming events from several services instead of joining across databases.
The price is eventual consistency: after a command the read model updates a moment later. The UI must tolerate this (return the created resource from the command, show "processing" states).
Simplest CQRS needs no broker: separate command and query handlers in code, same database, perhaps read-only views. Add separate stores only when needed.
Event sourcing
Instead of storing the current state, store the append-only sequence of events that led to it. Current state is derived by replaying them.
A bank account example:
public sealed interface AccountEvent {
record Opened(String accountId, long at) implements AccountEvent { }
record Deposited(long amountCents) implements AccountEvent { }
record Withdrawn(long amountCents) implements AccountEvent { }
}
public class Account {
private long balanceCents;
private boolean open;
/** Rebuild state from history. */
public static Account replay(List<AccountEvent> history) {
Account account = new Account();
history.forEach(account::apply);
return account;
}
/** Command: validate against current state, return new events (nothing is mutated here). */
public List<AccountEvent> withdraw(long amountCents) {
if (!open) {
throw new IllegalStateException("Account is not open");
}
if (amountCents <= 0 || amountCents > balanceCents) {
throw new IllegalArgumentException("Invalid withdrawal");
}
return List.of(new AccountEvent.Withdrawn(amountCents));
}
/** Apply an event: pure state change, no validation, no side effects. */
public void apply(AccountEvent event) {
switch (event) {
case AccountEvent.Opened e -> open = true;
case AccountEvent.Deposited e -> balanceCents += e.amountCents();
case AccountEvent.Withdrawn e -> balanceCents -= e.amountCents();
}
}
public long balanceCents() { return balanceCents; }
}
The separation is the heart of it: commands decide (and may be rejected), events are facts that have already happened and are applied without validation, so replaying history always works.
Benefits:
- Complete audit trail by design.
- Time travel and debugging: rebuild state as of any moment.
- New read models can be built later by replaying history.
- Natural source of integration events.
Costs and challenges:
- Event schema evolution: events are immutable and live forever; you need versioning and upcasters.
- Replay performance: use periodic snapshots so you load the latest snapshot plus recent events.
- Concurrency: use an expected version on append (optimistic locking) to detect conflicting writers.
- Querying is only practical through projections, so event sourcing nearly always comes with CQRS.
- Corrections require compensating events; privacy deletion (GDPR) needs crypto-shredding or careful design.
- Unfamiliar to most teams; learning curve and tooling (Axon Framework, EventStoreDB, Kafka-based stores).
System design considerations
- Version events from day one and plan upcasting for old events
- Use snapshots to bound replay time
- Make read models rebuildable from the event log
When to use it, and when not
Use it when
- Read and write loads or shapes are very different
- You need a complete audit trail or time travel
- You build read models from events for several consumers
Think twice when
- The domain is simple CRUD
- The team cannot yet handle eventual consistency and schema evolution
- You would add the patterns for fashion rather than need
When to use
| Use it when | Avoid it when |
|---|---|
| Audit history is a core requirement (finance, trading, ledgers) | Simple CRUD |
| Complex domain with many state transitions | Team lacks experience and the domain is not complex |
| Heavy read/write asymmetry | Strong immediate consistency is required for reads |
| Need multiple tailored read views | You would only be adding layers |
Common mistakes
- Applying CQRS/ES to a CRUD service because it sounds modern.
- Putting side effects (emails) in
apply, which re-executes on every replay. - Events named after data changes (
UserUpdated) instead of business facts (AddressChanged). - Treating the read model as authoritative for validation instead of the write side.
Interview questions
1. What is CQRS?
A pattern that splits the model for updating information (commands) from the model for reading it (queries), allowing each to be optimised and scaled independently.
2. What is event sourcing?
Persisting changes to application state as an immutable sequence of events and deriving the current state by replaying them.
3. Are CQRS and event sourcing the same?
No. CQRS is about separate read/write models; event sourcing is about how state is stored. They complement each other.
4. How do you handle eventual consistency in the UI?
Return the result of the command (id, new state), update the client optimistically, poll or push (WebSocket/SSE) for read-model updates, or show a "pending" indicator.
5. How are snapshots used?
A snapshot stores the aggregate state at version N. To load, read the latest snapshot and replay only events after N.
6. How do you change an event's structure?
Never edit stored events. Add a new event version and an upcaster that converts old versions on read, or use tolerant readers with optional fields.
7. Event sourcing vs outbox?
Event sourcing stores events as the source of truth. The outbox pattern stores a normal state change and an event record together to publish reliably. Many systems need only the outbox.
Check yourself
1What does CQRS separate?
2What does event sourcing store?
3What is a typical cost of CQRS with separate read models?
Key takeaways
- CQRS: different models for commands and queries; accept eventual consistency.
- Event sourcing: store facts, derive state; gain audit and replay, pay in complexity.
- Use them selectively, in the parts of the system that truly benefit.
Finished this page? Track your progress (saved only in your browser).