Skip to content

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.

CQRS: separate command (write) and query (read) models
Source: Wikimedia Commons. CC BY-SA 3.0.
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.

  • 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.