Event Sourcing & CQRS#
Problem statement (interviewer prompt)
Design a banking ledger that must replay every state change for audit. Use event sourcing for the write side; build multiple read models (SQL view, search index, cache). Cover snapshots, projection lag, event versioning, and when to use CQRS without event sourcing.
flowchart LR
C([Command])
WM[Write model<br/>commands -> events]
ES[(Event store)]
P[Projector]
RM[(Read model)]
Q([Query])
C --> WM --> ES
ES --> P --> RM
Q --> RM
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class C,WM,P,Q service;
class ES,RM datastore;
Event sourcing: state = sequential log of immutable events; the current state is a projection (replay). CQRS: separate write model (commands → events) from read model (queries over projections).
flowchart TB
subgraph Write[Write side - command]
C[Client]
CMD[Command handler]
AGG[Aggregate<br/>load events, decide, emit]
ES[(Event store<br/>append-only)]
end
subgraph Bus[Event bus]
KAFKA[Kafka / outbox]
end
subgraph Read[Read side - query]
PROJ[Projector / consumer]
RM1[(Read model 1<br/>SQL view)]
RM2[(Read model 2<br/>Elasticsearch)]
RM3[(Read model 3<br/>cache)]
Q[Query API]
end
C --> CMD --> AGG
AGG --> ES
ES --> KAFKA
KAFKA --> PROJ
PROJ --> RM1
PROJ --> RM2
PROJ --> RM3
C --> Q
Q --> RM1
Q --> RM2
Q --> RM3
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class C client;
class CMD,AGG,PROJ,Q service;
class ES,RM1,RM2,RM3 datastore;
class KAFKA queue;
Event sourcing - what + why#
- Every state change = an immutable event appended to the store.
- Current state = fold(events, initial).
- Replay events to rebuild state at any point in time → free audit log + time travel.
Account a-42 events
AccountOpened(owner=alice)
Deposited(100)
Deposited(50)
Withdrew(30)
→ balance = 120
CQRS - what + why#
- Command model: optimised for writes, enforces invariants.
- Query model: many denormalised projections, optimised per use case.
- Eventual consistency between them (often ms).
The two are independent ideas - you can do CQRS without event sourcing, and event sourcing without CQRS.
Aggregates + events#
sequenceDiagram
participant C as Client
participant H as Command Handler
participant A as Aggregate
participant E as Event Store
C->>H: Withdraw(account=42, amt=30)
H->>E: load events for 42
E-->>H: [Opened, Deposited(100), Deposited(50)]
H->>A: rebuild from events, then withdraw(30)
A-->>H: ok + new event Withdrew(30)
H->>E: append Withdrew(30) with expected version
E-->>H: ack
Snapshots#
Replaying 50k events per command is expensive. Persist a snapshot every N events; start replay from there.
Projection strategies#
- Synchronous in-tx: same DB; strong but couples write to all readers.
- Async via outbox + bus (most common): scalable, eventually consistent.
- On-demand: build a projection only when first queried.
Concurrency#
- Optimistic concurrency on append:
expectedVersion = N. Conflict → retry. - One aggregate = one consistency boundary; cross-aggregate ops go via sagas.
When to use#
- Strict audit / regulatory replay (banking, ledger, trading).
- Multiple read shapes over the same data.
- Long-running business processes where history matters.
When NOT to use#
- CRUD app with no audit need - adds complexity for nothing.
- Heavy schema churn - replaying old events with new code is painful.
- Reporting-only workloads - straight ETL is simpler.
Pitfalls#
- Event versioning: rename a field? Upcasters or never-rename rules.
- Replay performance: snapshots + idempotent projectors.
- Read-after-write: projections lag - pin reads to write-DB for a session, or push from write API.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Pub/Sub & message brokers | topics, consumer groups, delivery semantics | pub-sub-pattern |
HLD |
CAP / PACELC | C vs A under partition; L vs C otherwise | cap-pacelc |
HLD |
Idempotency & retries | safe re-execution, backoff + jitter | idempotency-retries |
HLD |
Event sourcing + CQRS | commands -> events; separate read model | event-sourcing-cqrs |
LLD |
Behavioural patterns | Strategy, Observer, State, Command, Chain | behavioral-patterns |
LLD |
Immutability | immutable types, persistent collections | immutability |
Quick reference#
Where you find this in the wild#
- Banking ledgers, trading systems (regulatory replay).
- Kafka Streams / Materialize.
- Axon Framework (JVM), EventStoreDB.
- Most CQRS systems in production are only CQRS - not event sourced.
Event schema rules#
- Events are immutable facts in past tense (
AccountOpened,OrderShipped). - Never delete; "delete" is a new event (
AccountClosed). - Add fields only; don't remove or rename. Use upcasters for forward migration.
- Encode in a stable format (Avro, Protobuf with schema registry).
Read model lag SLO#
- Define one - typical 1-5 s in production.
- Surface lag on dashboards; alert when > threshold.
Anti-patterns#
- Putting commands in the event log ("UserClickedSubmit" - commands are wishes, events are facts).
- Treating events as DTOs (they're domain facts; downstream views should not couple to them).
- Replaying with code that has business-logic changes that depend on time-of-replay.
Refs#
- Greg Young: "CQRS Documents" (free).
- Vaughn Vernon: Implementing Domain-Driven Design.
- Martin Kleppmann: Designing Data-Intensive Applications, ch. 11.
- EventStoreDB docs.
- "Event Sourcing You Are Doing It Wrong" - Martin Kleppmann talk.
FAQ#
What is event sourcing?#
Event sourcing stores every state change as an immutable event in an append-only log, and rebuilds current state by replaying those events on demand.
Is CQRS the same as event sourcing?#
No. CQRS only separates read and write models. Event sourcing is one way to power the write side, but CQRS can use a regular database just as well.
When should I use event sourcing?#
Use it when you need a full audit trail, time travel, or multiple read shapes from the same data, such as ledgers, order systems, or workflow engines.
What are snapshots in event sourcing?#
Snapshots cache the aggregated state at a recent event so replay starts from that point instead of replaying every historic event from the beginning.
What is projection lag?#
Projection lag is the delay between writing an event and updating the read model. Clients reading right after a write may see stale data briefly.
Related Topics#
- Pub/Sub Pattern: pub/sub messaging delivers the domain events that event-sourced systems produce and consume
- Distributed Transactions: the saga pattern in event sourcing replaces distributed transactions for long-running processes
- Change Data Capture: CDC can feed external systems with the same change stream that event sourcing captures internally
Further reading#
Curated, high-credibility sources for going deeper on this topic.
- ✍️ Blog - Greg Young - CQRS Documents (PDF)
- ✍️ Blog - Martin Fowler - Event sourcing
- ✍️ Blog - Martin Fowler - CQRS
- 📘 Book - Vaughn Vernon - Implementing Domain-Driven Design
- 📑 Docs - EventStoreDB documentation