Event Sourcing & CQRS — Detailed#
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 |