Event Sourcing & CQRS

Storing every change that happened instead of just the current state, and separating the model used to write data from the model used to read it.

What is it?

Most applications store only the current state: a bankAccount row with a balance column. Every deposit or withdrawal just overwrites that number. This works fine until someone asks: "how did this account end up at $340? What was the sequence of transactions?" With only the current balance saved, that history is simply gone — you'd have to have been logging it separately from the start, and even then, reconstructing "what the account looked like at 3pm last Tuesday" from an ad-hoc log is painful.

Event sourcing solves this by flipping what gets saved: instead of storing the current balance, you store the full sequence of things that happened — AccountOpened, Deposited($100), Withdrew($40) — as an ordered list of events. The current balance is never stored directly; it's calculated by replaying all the events from the beginning (or from a saved checkpoint). This gives you a complete, trustworthy history for free, the ability to see the state at any point in time, and the ability to fix a bug in your calculation logic and replay history to get corrected current state.

A separate but often-paired problem: in many real systems, the shape of data you want when writing (validate this transaction, apply business rules, ensure consistency) is very different from the shape you want when reading (a denormalized, pre-joined view optimized for a dashboard, searchable and fast). Forcing both to go through the exact same model is often either slow for reads or awkward for writes. CQRS (Command Query Responsibility Segregation) addresses this by using a different model for writes ("commands," which change state) than for reads ("queries," which just fetch data) — they can even live in entirely different databases, optimized independently.

Event sourcing and CQRS are frequently used together — events are the natural "write model," and a read-optimized view is built by projecting those events into whatever shape reads need — but they're separate ideas that solve separate problems, and either can be adopted without the other.

Explain like I'm 10

Event sourcing is like a bank keeping every transaction slip ever issued, rather than just a whiteboard with today's balance — the balance can always be recomputed, and you can answer 'what happened on March 3rd?' CQRS is like that same bank having a fast teller window for withdrawals (optimized for correctness) and a completely separate printed monthly statement (optimized for readability) — both describe the same account, built differently for different purposes.

Examples

Traditional state storage vs. event sourcing

// Traditional: only the current state is stored
// accounts table: { id: 1, balance: 60 }
// The history of how it got to 60 is gone.

// Event sourced: every change is stored, in order
const events = [
  { type: "AccountOpened", amount: 0 },
  { type: "Deposited", amount: 100 },
  { type: "Withdrew", amount: 40 },
];

function currentBalance(events) {
  return events.reduce((balance, event) => {
    if (event.type === "Deposited") return balance + event.amount;
    if (event.type === "Withdrew") return balance - event.amount;
    return balance;
  }, 0);
}

currentBalance(events); // 60 — same answer, but the full history is preserved

Both approaches agree the balance is 60, but only the event-sourced version can answer 'what was the balance right after the deposit?' or 'replay history with a corrected fee calculation.'

CQRS: separate models for writing and reading

// Write model (command side): enforces business rules, one account at a time
class AccountCommandHandler {
  withdraw(accountId, amount) {
    const events = eventStore.load(accountId);
    const balance = currentBalance(events);
    if (balance < amount) throw new Error("Insufficient funds");
    eventStore.append(accountId, { type: "Withdrew", amount });
  }
}

// Read model (query side): a denormalized view, rebuilt from events,
// optimized for a dashboard showing every account at once
class AccountSummaryProjection {
  onEvent(event) {
    if (event.type === "Withdrew") {
      dashboardDb.updateBalance(event.accountId, -event.amount);
    }
  }
}
// A dashboard query just reads dashboardDb directly — fast, pre-computed,
// and never touches the event store or the write-side business rules.

Withdrawing money goes through strict validation against the event history (the write/command side). Displaying a dashboard reads from a separate, pre-computed, denormalized store built by 'projecting' events as they happen (the read/query side) — each side is optimized for its own job.

How it works

In an event-sourced, CQRS system, a command (like "withdraw $40") first passes through validation against the current state — reconstructed by replaying events (often from a periodic snapshot so you don't replay the entire history every time). If valid, a new event is appended to an append-only event store. Separately, one or more projections listen for new events and update whatever read-optimized views they're responsible for — a search index, a dashboard table, a cache. Reads never go through the command path at all; they query the projection directly.

Command  --> [ validate against replayed state ] --> Event Store (append-only)
                                                             |
                                                             v
                                                     Projection(s) update
                                                     read-optimized views
                                                             |
  Query    <--------------------------------------- Read Model(s)

Why does it exist?

Event sourcing exists because "just the current state" throws away information many systems eventually need — audit trails, debugging ("how did we get into this bad state?"), and the ability to fix calculation bugs retroactively by replaying corrected logic over history. CQRS exists because forcing reads and writes through one shared model creates a lose-lose compromise: either the model is normalized and safe for writes but slow and awkward for the complex reads a UI wants, or it's denormalized and fast for reads but risks inconsistency for writes. Splitting them lets each side be optimized for what it actually needs to do.

When to use it

Reach for event sourcing when a full audit history is a real business requirement (financial ledgers, inventory changes, anything regulators or support teams need to reconstruct), or when "what happened, and in what order" is itself valuable data. Reach for CQRS when read and write patterns have genuinely diverged — heavy, complex reads (dashboards, search, reports) alongside a write side that needs strict, careful validation.

When not to use it

Both add substantial complexity: event sourcing means every query for "current state" needs replay logic (or snapshotting infrastructure) and schema changes to events must be handled carefully since old events never disappear; CQRS means running and keeping two models in sync, often with eventual consistency between them. For a typical CRUD application with straightforward, aligned read/write needs and no audit requirement, this is significant overhead for no real benefit — plain current-state storage is simpler and should be the default.

Common mistakes

  • Adopting event sourcing without a plan for handling changes to event schemas over time — old events were recorded under an old shape and can't be rewritten, only accommodated.

  • Assuming CQRS requires two separate databases and heavy infrastructure from day one, when a simpler version (two different query paths against related tables) can capture much of the benefit at far lower cost.

  • Using event sourcing purely 'because it's interesting' on a system with no real audit or replay need, rather than as a response to an actual requirement.

Practice exercises

  1. Easy:

    Given a list of events (ItemAdded, ItemRemoved) for a shopping cart, write a function that computes the current cart contents by replaying them.

  2. Medium:

    Design the events (names and payloads) you'd need to fully event-source a simple task-tracking app's task lifecycle (created, assigned, completed, reopened).

  3. Hard:

    For an e-commerce order system, describe how you'd split the write model (placing/cancelling orders) from a read model (a customer-facing order history page), including what a projection would need to do to keep the read model updated.

Interview questions

What problem does event sourcing solve that traditional current-state storage doesn't?

It preserves the complete history of how a system reached its current state, rather than only the final value — enabling audit trails, point-in-time reconstruction, and the ability to replay history with corrected logic.

What does CQRS stand for, and what does it actually separate?

Command Query Responsibility Segregation — it separates the model used to change state (commands, which enforce business rules) from the model used to read state (queries, which can be denormalized and optimized purely for fast, convenient reads).

Why are event sourcing and CQRS often used together, even though they're separate concepts?

Because the stream of events from an event-sourced write model is a natural source to build one or more read-optimized projections from, giving CQRS's read side a ready-made, complete feed of everything that changed.