Courses › System Design › Patterns

Event Sourcing & Read Models

Events as the source of truth, snapshots, projections and living with eventual consistency.

Most databases store current state: the account balance is 120, the order status is "shipped". Event sourcing stores what happened instead — every change as an immutable event — and derives current state by replaying them. CQRS (Command Query Responsibility Segregation) separates the model you write to from the models you read from. They're often used together, and both are powerful tools that are easy to over-apply.

Event sourcing

1  AccountOpened    { owner: "alice" }
2  MoneyDeposited   { amount: 200 }
3  MoneyWithdrawn   { amount: 50 }
4  MoneyDeposited   { amount: 30 }
→ current balance = 200 − 50 + 30 = 180

The event store is an append-only log, partitioned by entity (the *aggregate* — one account, one order). To handle a command:

  • Load the aggregate's events and replay them to get its current state.
  • Check the command against that state ("is the balance ≥ 50?").
  • Append new events — with an expected version, so two concurrent commands can't both append at version 4. The loser reloads and retries (optimistic concurrency).

What you gain

  • A complete audit trail for free. Finance, healthcare, inventory and anything regulated value this a lot.
  • Time travel. Rebuild state as of any moment ("what did this cart contain at checkout?").
  • New read models from old data. Replay all history into a projection that didn't exist when the events were written.
  • Natural integration. The events are already a stream other services can consume.

What it costs

  • Rebuilding state is slow for long-lived entities. Take a snapshot every N events (say 100) and replay only events after the latest snapshot.
  • Events are forever. You can't edit history, so changing an event's shape needs versioning (upcasters that translate old versions on read).
  • Deleting personal data (GDPR) is awkward in an immutable log. A common answer is *crypto-shredding*: encrypt personal fields per user and delete the key.
  • Querying across entities ("all accounts with a balance over 1,000") is impossible directly against the event store. That's where CQRS comes in.

CQRS

Use different models for writes and reads:

  • The command side validates and records changes (events, or normal rows) — normalized and built for correctness.
  • The query side holds one or more read models (*projections*), each shaped for a particular screen or query: a denormalized table, a search index, a cache, a materialized view.
[Command API] -> (Event store)
(Event store) -> {Event stream} -> [Projectors]
[Projectors] -> (Read DB: order summaries)
[Projectors] -> (Search index)
[Query API] -> (Read DB: order summaries)

You don't need event sourcing to use CQRS — a Postgres primary for writes plus an Elasticsearch index fed by CDC is already CQRS (see Building Full-Text Search). And many systems with read replicas or precomputed feeds are doing a light version of it (see Scaling Reads).

Living with eventual consistency

Projections update asynchronously, so a user can save a change and not see it on the next page load. Handle it explicitly:

  • Return the new state from the command and render it directly.
  • Read-your-writes: the command returns the event's version; the query waits (briefly) until the projection reaches it, or falls back to the write side.
  • Show "processing…" states for things like order placement, where a short delay is natural.

Projector rules

  • Idempotent and ordered per entity — track the last event version applied per projection, and skip anything at or below it.
  • Rebuildable — a new or broken projection is rebuilt by replaying from the start of the log into a fresh table, then switched over.
  • Monitored for lag — alert when a projection falls more than a few seconds behind the stream.

Storage choices

  • Postgres with an events table: (aggregate_id, version, type, payload, created_at) and UNIQUE(aggregate_id, version) to enforce optimistic concurrency. Simple and transactional; enough for most products.
  • Purpose-built stores (EventStoreDB) add subscriptions and projections natively.
  • Kafka is a great *distribution* channel for events, but a poor event store on its own: it can't efficiently load "all events for account 42", and it can't do a conditional append on a version.

When to use it — and when not

[!TIP] Name it, then scope it In an interview, say where you'd use it *and* where you wouldn't: "I'd event-source the ledger, because the history *is* the product. The user profile service stays plain CRUD."

[!INTERVIEW] What a strong answer sounds like "Orders are event-sourced: OrderPlaced, PaymentCaptured, ItemShipped and so on, appended to a Postgres events table with a unique (order_id, version), so concurrent commands conflict instead of overwriting. We snapshot every 50 events. Projectors consume the stream to build an order-summary table for the customer UI and an index for support search; both are idempotent by version and can be rebuilt from the log. The UI returns the new order state from the command, so the customer doesn't notice the projection lag."