Event-driven architecture: sagas, CQRS and event sourcing

How services coordinate without distributed transactions: events, sagas with compensation, separate read and write models, and storing changes instead of state.

On the roadmap:Advanced · step 5: Event-driven designAdvanced · step 6: CQRS

Lesson 3 of 6 · 14 min read · quiz · exercise

Events as the connective tissue

In an event-driven design, services announce facts that happened ("OrderPlaced", "PaymentCaptured") and others react. This decouples services in time and knowledge: the publisher does not know who listens. The price is that the flow is spread over many services, so you need discipline about event design, ordering, retries (previous lesson) and observability.

Three patterns come up constantly.

Saga: transactions across services

In a microservice system each service owns its database, so a single ACID transaction across them is not available (two-phase commit across services is slow and fragile). A saga is a sequence of local transactions, each followed by an event or command that triggers the next. If a step fails, the saga runs compensating actions for the steps already done.

Figure SDA-MSV-03-1 · Order saga with compensation · © forcsguy.com

Two styles:

  • Choreography: services react to each other's events with no central controller. Simple for short flows; hard to follow and to change as it grows (nobody owns the whole flow).
  • Orchestration: a coordinator (saga orchestrator or a workflow engine) tells each service what to do and tracks state. Easier to understand, monitor and change; the coordinator is an extra component to build and keep available.

Rules: every step and every compensation must be idempotent; saga state must be persisted so it survives crashes; and be explicit about isolation, since other requests can see intermediate states (use a "pending" status and semantic locks).

CQRS: separate reads from writes

Command Query Responsibility Segregation uses different models for changing data (commands) and for reading it (queries).

Figure SDA-MSV-03-2 · CQRS with a read model · © forcsguy.com

Why: reads and writes often differ in shape (normalised writes, denormalised reads) and scale (reads vastly outnumber writes). Cost: extra moving parts and eventual consistency. Use it where the gap between read and write needs is real, not as a default. A light form (one database, separate query code and views) captures much of the benefit.

Event sourcing: store the changes

Instead of storing the current state, store the sequence of events that led to it, as an append-only log. Current state is derived by replaying events (with snapshots for speed).

type Event = { type: "Deposited" | "Withdrawn"; amount: number };
const apply = (balance: number, e: Event) => e.type === "Deposited" ? balance + e.amount : balance - e.amount;
const balance = events.reduce(apply, 0);          // state = fold over history

Benefits: a perfect audit trail, ability to rebuild any view (including new ones) from history, debugging by replay, and natural fit with CQRS projections.

Costs: more complex to build and query, event schema evolution over years, handling of mistakes by compensating events (never edit history), eventual consistency of views, and privacy rules (erasing personal data in an immutable log needs design, such as encryption keys per person). Event sourcing suits domains where history is the point (finance, ledgers, workflows); it is overkill for plain CRUD. (A ledger in the payment lessons is the same idea.)

Using them together, and not

SituationReach for
Business process spanning several servicesSaga (orchestrated if non-trivial)
Read load and shape differ greatly from writeCQRS
Need full history and rebuildable viewsEvent sourcing
Simple CRUD within one serviceNone of these

These patterns add power and complexity. Start with the simplest design (a modular monolith or a few services with synchronous calls), and adopt them where a concrete problem demands, ideally one at a time.

Event design tips

  • Name events in the past tense after business facts.
  • Include ids, timestamps and a version; avoid leaking internal tables.
  • Prefer event-carried state for what consumers need, to avoid chatty callbacks, but beware coupling.
  • Keep a schema registry and evolve compatibly.
  • Make the flow observable: carry a correlation id through every event (see the observability lesson).

Real-world reading

Check your understanding

Question 1 of 3

0 correct so far

Why use a saga instead of a distributed transaction across services?

Go deeper

Topics covered in this lesson

All topics

© 2026 forcsguy.com. Original lesson text and diagrams. Books and sites linked above belong to their authors.