Skip to content

Event Sourcing vs CQRS

Core Concept LearningJuly 4, 20265 min readUpdated July 21, 2026

A customer sees “payment pending” after checkout, retries, and is charged twice. The design question is not whether the system uses fashionable patterns; it is whether commands reject duplicates, history explains what happened, and the read view tells the truth while it catches up.

Event Sourcing records domain changes as events and derives aggregate state from them. CQRS separates command and query models; it does not require Event Sourcing, separate databases, or asynchronous projections. This guide compares their distinct jobs, follows one order command into a read model, and defines when simpler CRUD is the safer choice.

Place both patterns in the wider event-driven architecture map. For broker trade-offs behind asynchronous projections, compare Kafka, RabbitMQ, and SQS.

Event Sourcing and CQRS architecture in Go featuring append-only PostgreSQL event stores, optimistic concurrency, and async read projections
Event Sourcing and CQRS architecture in Go featuring append-only PostgreSQL event stores, optimistic concurrency, and async read projections

Event Sourcing

In CRUD, updating an order row replaces selected current values; history exists only if you model it separately. Event Sourcing instead appends facts such as OrderCreated, ItemAdded, and PaymentAuthorized, then folds an aggregate's stream to recover current decision state. Snapshots and projections may store derived state, but the retained events remain the authoritative record.

An event store needs per-stream ordering and expected-version checks so concurrent commands cannot silently overwrite each other. EventStoreDB and Marten provide event-sourcing semantics directly; Kafka can carry retained events, but aggregate streams, optimistic concurrency, replay boundaries, and retention still need deliberate design. Snapshot frequency should follow measured replay cost, not a fixed event count.

Skimmer: these patterns answer different questions and can be combined.
PatternUse whenTrade-off
CRUDCurrent state and read/write shapes are straightforwardSimple path; history and integration delivery need explicit additions
CQRSCommands and queries need materially different modelsClear separation; two models and possible synchronization
Event SourcingDomain facts and historical reconstruction are requirementsReplayable history; schema, replay, and retention obligations
Event Sourcing and CQRS architecture in Go featuring append-only PostgreSQL event stores, optimistic concurrency, and async read projections
Event Sourcing and CQRS architecture in Go featuring append-only PostgreSQL event stores, optimistic concurrency, and async read projections

Quick reference

  • Best for: audit requirements, complex domain lifecycles, temporal queries, event-driven systems.
  • Strengths: explicit domain history, historical reconstruction within retained data, and new projections from old events.
  • Weaknesses: query complexity, eventual consistency, learning curve, snapshot management.
  • Treat recorded events as immutable facts; corrections use new events, while privacy and retention rules may require redaction or cryptographic erasure strategies.
  • Snapshot only when measured replay cost justifies another derived artifact; verify that a missing or corrupt snapshot falls back to event replay.
  • Schema evolution needs versioned readers or upcasters so retained historical events remain interpretable.
Unsafe append — no concurrency guard
1// TypeScript-like event-store interface2const stream = await store.load("order-42");3const order = Order.rehydrate(stream.events);4const event = order.place(command);5await store.append("order-42", [event]);6// Two writers can both load version 7 and append conflicting facts.
Expected-version append
1const stream = await store.load("order-42");2const order = Order.rehydrate(stream.events);3const event = order.place(command);4 5try {6  const newVersion = await store.append(7    "order-42",8    stream.version, // expected version, for example 79    [{ ...event, commandId: command.id }],10  );11  return { orderId: "order-42", version: newVersion };12} catch (error) {13  if (error instanceof WrongExpectedVersion) {14    return { status: 409, code: "order_changed_retry_command" };15  }16  throw error;17}18// Check: two appends expecting version 7 → one succeeds, one returns 409.

Remember this

Two appends expecting the same version means exactly one succeeds and one returns 409 — the expected-version check is what makes concurrent writes to one aggregate safe, and a corrupt snapshot must still fall back to full event replay.

CQRS

CQRS separates the model that accepts commands from the model that answers queries. The command side expresses intent and enforces invariants; the query side returns a shape designed for the caller. Both sides can begin in one process and one database. Separate stores and asynchronous projections are deployment choices, not part of the definition.

When the query model is built asynchronously, it can lag after a successful command. The API should expose that state honestly—for example, return the accepted command ID and let the client poll until the projection reaches that version. For a small CRUD screen whose read and write shapes are already simple, CQRS creates vocabulary and synchronization work without removing a bottleneck.

Event Sourcing and CQRS architecture in Go featuring append-only PostgreSQL event stores, optimistic concurrency, and async read projections
Event Sourcing and CQRS architecture in Go featuring append-only PostgreSQL event stores, optimistic concurrency, and async read projections

Quick reference

  • Best for: command invariants and query shapes that genuinely need different models.
  • Strengths: optimized read and write paths, independent scaling, multiple read models.
  • Weaknesses: two models to maintain; eventual consistency only when synchronization is asynchronous.
  • Write side: authenticate, validate commands, enforce invariants, and return conflict or not-found outcomes explicitly.
  • Read side: return a view or null; if a required projection version is not ready, return pending/retry metadata or use a defined authoritative fallback.
  • Do not use CQRS for simple CRUD — the overhead is not justified.
Command side — intent and invariant
1async function handlePlaceOrder(command: PlaceOrder) {2  const existing = await orders.find(command.orderId);3  if (existing) return { status: 409, code: "order_exists" };4  if (command.lines.length === 0) {5    return { status: 422, code: "empty_order" };6  }7 8  await orders.insert(Order.place(command));9  return { status: 201, orderId: command.orderId };10}
Query side — caller-shaped view
1async function getOrderSummary(orderId: string) {2  const row = await orderSummaries.find(orderId);3  if (!row) return { status: 404, code: "order_not_found" };4  return {5    status: 200,6    body: {7      orderId: row.id,8      status: row.status,9      total: row.total,10    },11  };12}13 14// Check: PlaceOrder returns 201; the query returns its compact view.15// With an async projector, return pending until its version catches up.

Remember this

CQRS separates reads from writes — use it when query patterns differ significantly from write patterns.

Using Event Sourcing and CQRS Together

Together, the flow is concrete: PlaceOrder loads the order stream, rejects a duplicate command ID or stale expected version, appends OrderPlaced, and returns the new stream version. A projector consumes that event idempotently and updates OrderSummary. A query may return the summary, null for an unknown order, or a pending response when its projection version trails the command result.

Read models are rebuildable only if events are retained, schemas remain readable, side effects are separated from replay, and the rebuild has been tested. Email must not be resent merely because a projection replays. The TypeScript-like pseudocode below makes duplicate delivery and unsupported events visible instead of assuming a perfect consumer.

Event Sourcing and CQRS architecture in Go featuring append-only PostgreSQL event stores, optimistic concurrency, and async read projections
Event Sourcing and CQRS architecture in Go featuring append-only PostgreSQL event stores, optimistic concurrency, and async read projections

Quick reference

  • Event store = write model. Projections = read models. Events connect them.
  • Rebuild read models only from a known checkpoint with retained, readable events.
  • Frameworks: EventStoreDB, Axon Framework (Java), Marten ( .NET), Eventuate.
  • Start simple: CRUD → CQRS (if read/write diverge) → Event Sourcing (if audit needed).
  • Projections can lag — design UI for eventual consistency (optimistic updates, polling).
  • Test duplicate delivery, poison events, checkpoint loss, and rebuilds without replaying external side effects.
Unsafe projector — duplicate delivery mutates twice
1async function project(event: OrderPlaced) {2  await summaries.insert({3    orderId: event.orderId,4    status: "placed",5  });6  await email.sendConfirmation(event.orderId);7}
Idempotent projector with explicit failure
1async function project(event: DomainEvent): Promise<void> {2  try {3    await db.transaction(async (tx) => {4      if (await tx.processedEvents.exists(event.id)) return;5 6      if (event.type !== "OrderPlaced") {7        throw new UnsupportedEventError(event.type);8      }9 10      await tx.orderSummaries.upsert({11        orderId: event.orderId,12        status: "placed",13        streamVersion: event.streamVersion,14      });15      await tx.processedEvents.insert(event.id);16    });17  } catch (error) {18    logger.error({ error, eventId: event.id }, "projection failed");19    throw error; // Let the broker retry or dead-letter by policy.20  }21}

Remember this

Combine them when you need both audit history and optimized reads — the event log feeds multiple read projections.

When NOT to Use These Patterns

A standard CRUD model with an audit table or outbox is often enough. Adding Event Sourcing to a simple task list creates replay, schema-evolution, and operational obligations. Adding CQRS when commands and queries use nearly the same shape creates two code paths without a compensating design benefit.

Use a requirement flow instead of a scale threshold. Need historical reconstruction from domain facts? Evaluate Event Sourcing. Need materially different command invariants and query shapes? Evaluate CQRS. Need reliable integration events but current-state CRUD remains appropriate? Use an outbox. If the requirement can be tested with a simpler model, keep the simpler model.

Quick reference

  • Skip Event Sourcing if: no audit requirement, simple state, team lacks DDD experience.
  • Skip CQRS if reads and writes have similar shapes and one consistency model serves both.
  • CRUD plus an audit table or outbox can preserve history and publish events without Event Sourcing.
  • Migration path: start CRUD, extract CQRS for hot read paths, add events when audit is required.
  • Complexity budget: every pattern must earn its place with a concrete requirement.
  • Interview tip: mention these patterns, explain trade-offs, but default to simpler solutions.

Remember this

Default to CRUD — adopt Event Sourcing and CQRS only when specific requirements justify the complexity.

Key takeaway

Event Sourcing answers “what facts define state over time?” CQRS answers “should commands and queries use different models?” They can be combined, but neither implies the other. Default to CRUD, an audit table, or an outbox until replayable domain history or divergent models provide a concrete benefit.

Practice (25 min): Model PlaceOrder as CRUD plus outbox, CQRS without Event Sourcing, and both patterns together; write the expected duplicate-command, missing-order, and stale-projection result for each. Intentionally replay an event and retry the projector after a simulated failure. Recover with idempotent side effects and a durable checkpoint, then state the requirement that would make you switch designs. Pass when replay rebuilds the read model without sending a second email.

Share:

Related Articles

Checkout hangs because payment is slow — and the order service is blocked waiting on a synchronous call. Event-driven ar

Read

Most .NET projects start clean and become entangled within six months. Controllers call repositories that call other ser

Read

Traditional CRUD (Create, Read, Update, Delete) database architectures mutate entity records in place using SQL UPDATE q

Read

Keep learning

Follow a structured path or browse all courses to go deeper.