Event Sourcing & CQRS - Complete Deep Dive

Prerequisites: Message Queues, Database Sharding Used in: Stock Broker, Digital Wallet


What is Event Sourcing?

Instead of storing the current state of data, you store every event (change) that happened to it. The current state is derived by replaying all events.

Real-world analogy: Think of your bank statement. The bank doesn’t just store “balance = $5,000”. It stores every transaction: +$3000 salary, -$50 groceries, -$200 rent, +$2250 refund. Your balance is computed from replaying all those transactions. If there’s a dispute, they can trace exactly what happened.

Traditional (state-based):
  UPDATE account SET balance = 5000 WHERE user_id = 123
  (Previous states are lost forever)

Event Sourced:
  Event 1: AccountCreated { user: 123, balance: 0 }
  Event 2: MoneyDeposited { user: 123, amount: 3000 }
  Event 3: MoneyWithdrawn { user: 123, amount: 50 }
  Event 4: MoneyWithdrawn { user: 123, amount: 200 }
  Event 5: MoneyDeposited { user: 123, amount: 2250 }
  Current balance = replay all events = $5000

What is CQRS?

CQRS (Command Query Responsibility Segregation) means separating the write path (commands) from the read path (queries) into different models, often different databases.

flowchart LR
    subgraph Traditional
        A[Same Database<br/>reads AND writes<br/>Same schema]
    end

    subgraph CQRS
        B[Write Model<br/>Commands<br/>Normalized<br/>Append-only] -->|"event sync"| C[Read Model<br/>Queries<br/>Denormalized<br/>Pre-computed]
    end

    classDef service fill:#10b981,stroke:#065f46,color:#fff
    classDef data fill:#fbbf24,stroke:#92400e,color:#000
    class A data
    class B service
    class C data

How Event Sourcing + CQRS Work Together

flowchart TD
    subgraph Write["WRITE SIDE"]
        A[Command] --> B[Validate]
        B --> C[Produce Event]
        C --> D[Append to Event Store]
    end
    
    subgraph Store["EVENT STORE"]
        D --> E["Event 1 - Event 2 - Event 3 ... Event N<br/>Kafka topic or EventStoreDB or DynamoDB"]
    end

    subgraph Read["READ SIDE"]
        E --> F["Projection 1: User Balance - Redis"]
        E --> G["Projection 2: Transaction History - Postgres"]
        E --> H["Projection 3: Analytics - ClickHouse"]
        E --> I["Projection 4: Search Index - Elasticsearch"]
    end

    classDef service fill:#10b981,stroke:#065f46,color:#fff
    classDef data fill:#fbbf24,stroke:#92400e,color:#000
    classDef async fill:#818cf8,stroke:#4338ca,color:#fff
    class A,B,C service
    class D,E data
    class F,G,H,I async

Key Concepts

Event Store

The single source of truth. An append-only log of all events. Never update, never delete.

Event Store (ordered, immutable):
┌──────┬─────────────────────┬───────────────────────────────────────┐
│ Seq  │ Timestamp           │ Event                                 │
├──────┼─────────────────────┼───────────────────────────────────────┤
│ 1    │ 2024-01-01 10:00:00 │ AccountCreated { id: A1, owner: Bob } │
│ 2    │ 2024-01-01 10:05:00 │ MoneyDeposited { id: A1, amt: 1000 } │
│ 3    │ 2024-01-01 11:00:00 │ MoneyWithdrawn { id: A1, amt: 200 }  │
│ 4    │ 2024-01-01 12:30:00 │ MoneyTransferred { from: A1, to: A2} │
└──────┴─────────────────────┴───────────────────────────────────────┘

Commands vs Events

  Commands Events
Tense Imperative (“TransferMoney”) Past tense (“MoneyTransferred”)
Can fail? Yes (validation) No (already happened)
Mutable? N/A Immutable
Example CreateOrder { items: [...] } OrderCreated { orderId: 123 }

Projections

Materialized views built from events. Each projection is optimized for a specific read pattern.

Same events → multiple projections:

Events: [OrderCreated, ItemAdded, ItemAdded, OrderPaid, OrderShipped]

Projection 1 (Order Status API):
  { orderId: 123, status: "shipped", total: $50 }

Projection 2 (Analytics Dashboard):
  { daily_orders: 1542, revenue: $78,000 }

Projection 3 (Search Index):
  { orderId: 123, items: ["book", "pen"], searchable: true }

Replay

Since all events are stored, you can:


Kafka as an Event Store

Kafka is commonly used as the event store because:

flowchart TD
    subgraph Topic["Kafka Topic: wallet-events"]
        P0["Partition 0 user_id mod N = 0<br/>ev1 - ev2 - ev5 - ev8..."]
        P1["Partition 1 user_id mod N = 1<br/>ev3 - ev4 - ev6 - ev9..."]
        P2["Partition 2 user_id mod N = 2<br/>ev7 - ev10 - ev11..."]
    end

    classDef async fill:#818cf8,stroke:#4338ca,color:#fff
    class P0,P1,P2 async

Properties:


Types / Approaches Comparison

Approach Write Model Read Model Sync Mechanism Consistency
Simple CRUD Same DB Same DB N/A (one model) Strong
CQRS only Write DB Read replicas DB replication Eventual
Event Sourcing only Event store Derive from events Replay Strong (on replay)
Event Sourcing + CQRS Event store Separate read DBs Event consumers Eventual

When to Use

Use Event Sourcing When:

Use CQRS When:

When NOT to Use:


Real-World Examples

Company Use Case
Stripe Every payment state change is an event. Enables dispute resolution, refunds, and audit trails.
LinkedIn Uses Kafka as central event log for all data changes. Services build their own projections.
Uber Trip lifecycle events (requested, matched, started, completed). CQRS for trip tracking vs billing.
Event Store Ltd Created EventStoreDB specifically for event sourcing.
Goldman Sachs Trade events for regulatory compliance and audit.

Common Interview Questions

Q: “Why not just use a regular database with an audit log table?” A: An audit log is a secondary artifact — it can drift from reality. With event sourcing, the event log IS the source of truth. You can’t have inconsistency between state and history because state is derived from history. Also, with event sourcing you can rebuild projections, create new read models retroactively, and replay for debugging.

Q: “How do you handle the fact that replaying millions of events to get current state is slow?” A: Use snapshots. Periodically save the current state (e.g., every 1000 events). To rebuild, load the latest snapshot and replay only events after it. For example: Snapshot at event 10,000 (balance=$5000) → replay events 10,001 to 10,050 → current state.

Q: “What about eventual consistency between write and read models?” A: Yes, there’s a propagation delay (usually milliseconds to seconds). For most use cases this is fine. If a user MUST read their own write immediately, use “read-your-writes” consistency: after a command, read from the write model (event store) until the projection catches up.

Q: “How do you handle schema evolution when event formats change?” A: Use event versioning. Store a version field with each event. Consumers (projections) must handle all versions via upcasting — transforming old event formats to new ones on read. Never modify stored events.

Q: “What’s the difference between Event Sourcing and Change Data Capture (CDC)?” A: CDC captures changes FROM a database (after the fact). Event Sourcing captures changes AS the primary storage (events are the source of truth, not derived from a DB). CDC is a way to get events out of a traditional DB. Event Sourcing means events ARE your DB.


← Back to Fundamentals Next: API Design →

💬 Comments