Architecture pattern comparison

Event sourcing vs CQRS

These two patterns show up together so often in blog posts and conference talks that many engineers assume they're one thing. They're not: event sourcing decides how you persist state; CQRS decides how you split reading state from writing it. Understanding the difference stops you from adopting more complexity than your problem actually needs.

TL;DR

  • Event sourcing stores every change as an immutable event, and derives current state by replaying the event log — instead of overwriting a row in place.
  • CQRS splits your write model (commands that change state) from your read model (queries that fetch state), often using entirely different data stores optimised for each.
  • Combined, they're powerful but not required together. Event sourcing gives you a perfect audit trail; CQRS gives you fast, purpose-built reads. Adopt each on its own merits.

Side-by-side comparison

AspectEvent sourcingCQRS
What it governsHow state is persisted (events, not rows)How reads and writes are routed and modelled
Source of truthThe append-only event logWhatever the write model defines — can be event-sourced or a normal table
Current stateDerived by replaying/folding eventsServed directly from a dedicated, pre-built read model
Query performancePoor without a projection — must replay historyFast by design — read models are shaped for the query
Audit trailFree — every change is a permanent, ordered recordNot inherent — depends on whether the write side also logs events
Consistency modelStrongly consistent within one aggregate's event streamRead model is typically eventually consistent with the write model
Can exist alone?Yes — with a single combined read/write modelYes — with a conventional CRUD write model, no event log
Common pairingEvent store (EventStoreDB, Kafka) + projectionsCommand handler (write DB) + materialized view / search index (read DB)
Rollback / time travelTrivial — replay up to any point in the logNot applicable on its own; depends on the write side

Code side-by-side

Recording a bank account withdrawal and answering "what's the balance?":

Event sourcing (no CQRS)

// Write: append an event, nothing is overwritten
eventStore.append("account-42", {
  type: "MoneyWithdrawn",
  amount: 50,
  at: "2026-07-05T10:00:00Z",
});

// Read: replay ALL events to derive balance
function getBalance(accountId) {
  const events = eventStore.read(accountId);
  return events.reduce((balance, e) => {
    if (e.type === "MoneyDeposited") return balance + e.amount;
    if (e.type === "MoneyWithdrawn") return balance - e.amount;
    return balance;
  }, 0);
}
// Fine for one account; slow across millions

CQRS (with a projection)

// Command side: handles the write, emits an event
function withdraw(accountId, amount) {
  validateSufficientFunds(accountId, amount);
  eventBus.publish({
    type: "MoneyWithdrawn", accountId, amount,
  });
}

// Projection: keeps the read model up to date
onEvent("MoneyWithdrawn", (e) => {
  readDb.query(
    "UPDATE account_balances SET balance = balance - $1 WHERE id = $2",
    [e.amount, e.accountId]
  );
});

// Query side: fast, direct read — no replay
function getBalance(accountId) {
  return readDb.query(
    "SELECT balance FROM account_balances WHERE id = $1",
    [accountId]
  );
}

When to use event sourcing

  • You need a complete, tamper-evident audit trail. Financial ledgers, compliance-heavy domains, and insurance claims benefit from an immutable record of every change and why it happened.
  • Debugging "how did we get here" incidents matters. Replaying the exact event sequence that led to a bad state is far easier than reconstructing it from row diffs or logs.
  • You expect to add new views of the same history later. New analytics or reporting requirements can be answered by writing a new projection and replaying existing events — no data migration required.
  • The domain is naturally event-shaped. Order processing, workflow engines, and inventory systems often map cleanly onto "things that happened" rather than "current row values."

When to use CQRS

  • Read and write workloads have very different shapes or scale. A product catalogue with millions of reads per write benefits from a denormalised, cache-friendly read model decoupled from the write model.
  • You need multiple, differently-shaped views of the same data. A search index, a reporting dashboard, and a mobile API can each get their own optimised read model fed from the same write side.
  • Write-side business logic is complex and read-side needs are simple. Keep validation and invariants in the command handler; let the read model be a flat, denormalised projection with none of that complexity.
  • You can tolerate eventual consistency on reads. If a user seeing their own change appear a few hundred milliseconds late is acceptable, CQRS's async projection lag is a non-issue.

English phrases engineers use

Event sourcing conversations

  • "We can replay the event stream to see exactly what happened before the outage."
  • "That's an append-only log — we never mutate past events."
  • "Let's add a snapshot so we don't replay 2 million events on every read."
  • "The aggregate is rebuilt by folding all its events in order."
  • "This bug is a data migration nightmare in a normal DB, but trivial here — just replay with the fix."

CQRS conversations

  • "The read model is lagging — check the projection's consumer offset."
  • "We keep the command side thin and push all the query complexity into projections."
  • "That's a read-your-writes problem — the UI queried before the projection caught up."
  • "Let's rebuild the projection from scratch after the schema change."
  • "Don't put business logic in the query side — it's just a materialized view."

Quick decision tree

  • Need a permanent audit trail / regulatory history → Event sourcing
  • Read and write loads have wildly different shapes or scale → CQRS
  • Both needs apply (audit trail + fast, varied reads) → Event sourcing + CQRS together
  • Simple CRUD app, one query shape, low traffic → Neither — plain database, plain model
  • Need multiple different read views of the same data → CQRS with several projections
  • Team is new to the pattern and timeline is tight → Start without CQRS; add it when a real read bottleneck appears

Frequently asked questions

Are event sourcing and CQRS the same pattern?

No, though they are frequently used together. Event sourcing is about how you store state: instead of saving the current row, you store every state-changing event and derive current state by replaying them. CQRS (Command Query Responsibility Segregation) is about how you serve reads and writes: you split the model that handles writes (commands) from the model that serves reads (queries), often backed by different data stores. You can do CQRS with a normal database and no event log, and you can do event sourcing with a single combined read/write model.

Why do people combine event sourcing with CQRS so often?

Event sourcing naturally produces a stream of events, but replaying the full stream to answer a query (e.g. "show me this customer's current balance") is slow. CQRS solves that by projecting the event stream into one or more purpose-built read models (materialized views) that are optimised for the exact queries the application needs — so reads never touch the event log directly.

What is a "projection" in this context?

A projection is a process that consumes events from the event store, in order, and updates a read-optimised representation — a SQL table, a search index, a cache entry. If you need a new query shape later, you write a new projection and replay the existing event history to build it, without touching the source of truth (the event log).