Skip to content
aipatterns.dev

Design patternData Patterns

Saga

Run a multi-step transaction as local steps, each with a compensation; on failure, compensate the completed steps in reverse - consistency by explicit undo, not locks.

Complexity
High
Seen in the wild
Medium
Also known as
Compensating Transaction

Intent

A workflow that spans services or non-transactional systems cannot share one database transaction. A saga splits it into a sequence of steps, each committing locally, and gives every step a compensation - the operation that semantically undoes it. If a step fails, the saga runs the compensations of all completed steps in reverse order. The outcome is either every step done, or every effect undone: consistency by construction, with no locks held across services.

Forces

  • Atomicity vs autonomy: each service owns its data and commits immediately, so intermediate states are visible to everyone until the saga finishes.
  • Rollback vs compensation: databases undo; the world does not - a sent email has no unsend, so compensations must be designed per step, not assumed.
  • Isolation vs concurrency: concurrent sagas can see each other's half-done work; without countermeasures, one saga reads a total another saga will later refund.
  • Coordination vs coupling: a central orchestrator sees the whole flow and its failures; pure choreography couples every service to every event.

Therefore

Write each step as an action plus a compensation, execute steps in order, and on the first failure run the compensations of the completed steps in reverse - then record the outcome so the saga's result is queryable.

Two coordination styles exist: orchestrated, where one component drives the steps and owns the failure path (the shape below), and choreographed, where services react to each other's events. Start orchestrated - the failure path is the hard part, and one place that owns it is easier to make correct than five. Persist the saga's state with Command-style records so a crashed process resumes instead of re-running side effects.

Structure

Diagram source (Mermaid)
sequenceDiagram
  participant Saga
  participant Stock
  participant Card
  participant Email
  Saga->>Stock: reserve(order)
  Stock-->>Saga: reservation
  Saga->>Card: charge(order)
  Card-->>Saga: receipt
  Saga->>Email: schedule confirmation
  Email-->>Saga: delivery failed
  Saga->>Card: refund(receipt)
  Saga->>Stock: release(reservation)

Participants

  • Saga (CheckoutSaga): owns the step list and the failure path. Runs actions in order; on failure, runs compensations in reverse and records the outcome.
  • Step (Step): one action plus its compensation. The compensation takes the action's result - refunding needs the receipt, releasing needs the reservation.
  • Local services (Stock, Card, Email): each commits its own step immediately. They know nothing about the saga.
  • Saga log (in the demo, outcome): the persisted record of what completed and what was compensated, so the result is auditable and resumable.

Use it when

  • A workflow spans services or systems that cannot share a transaction: reserve stock, charge a card, send a confirmation.
  • You can define a compensation for every step - refund a charge, release a reservation. If any step cannot be undone, handle it last or make it retriable instead.
  • The flow is long or slow enough that holding a distributed lock would be worse than brief visibility of intermediate states.
  • Steps are idempotent or can carry an idempotency key (Idempotent Consumer), because retries after crashes will re-run them.

Do not use it when

  • All the data lives in one database. A local ACID transaction is strictly simpler and gives real isolation; a saga re-implements it badly.
  • A step has no compensation - filing a legal record, dispatching a one-way physical shipment. Put non-compensable steps last, make them retriable until they succeed, or take the failure to a human workflow.
  • Strict isolation is required - concurrent sagas would double-spend a balance. Countermeasures (semantic locks, commutative updates) add complexity that a transaction gives you for free.
  • The workflow is two steps and rarely fails. An if with an explicit undo is a saga with none of the machinery; earn the structure first.

Application grill

Inspect first: the multi-step flows, their failure points, and whether each step can run twice (they must survive the idempotent-consumer bar); where saga progress would live (a table in the existing database is the default); existing queues, schedulers or workflow engines (Temporal, Step Functions) already in the organization.

Missing pieces: saga state: one table (saga id, step, status) - no new infrastructure. Execution: a worker or cron to advance and time out sagas - platform cron or the existing queue. An orchestration engine (Temporal, AWS Step Functions) buys durable retries and visibility at the cost of a platform to operate; it earns its keep past a handful of long-running flows.

Ask before you build:

  • Scale: how many concurrent sagas, and what timeout window precedes compensation?
  • Performance: must steps run strictly in order, or can independent steps parallelize?
  • Reliability: when compensation itself fails, what happens - alert plus a manual runbook, or automatic retry?
  • Budget: a workflow platform versus a hand-rolled state machine - how many flows justify the platform?
  • Implementation: how is every step made idempotent, and who owns the compensation tests?

Implementation steps

  1. List the steps in order and write the compensation next to each. If a compensation cannot be named, that step moves last or becomes a retriable action - decide before writing code.
  2. Make every action idempotent (or keyed), because crashes and retries re-run steps; a compensated-then-retried charge must not double-charge.
  3. Model a step as the pair { action, compensate } where compensation consumes the action's result. Type it once; every step obeys it.
  4. Drive the saga from persisted state, not from a live stack: write "step N completed with result R" before running step N+1, so a restart resumes instead of repeating.
  5. On failure, run compensations in strict reverse order, each with its step's recorded result, and continue compensating even if a compensation itself fails - log the failure and keep going.
  6. Record the terminal outcome (completed, compensated at step N, and why). A saga whose outcome cannot be queried is a rumor.
  7. Choose orchestration first; move to choreography only when independent workflows need to react to the same events anyway.

Example

Checkout across three services. Stock and card succeed; the confirmation email fails. The saga refunds the charge, then releases the stock - compensations in reverse - and records the outcome.

type Step = {
  name: string;
  action: () => string; // returns the result its compensation consumes
  compensate: (result: string) => string;
};

class CheckoutSaga {
  private executed: { step: Step; result: string }[] = [];
  outcome = "";

  constructor(private readonly steps: Step[]) {}

  run(): string {
    for (const step of this.steps) {
      try {
        const result = step.action();
        this.executed.push({ step, result });
        console.log(`  ok: ${step.name} -> ${result}`);
      } catch (e) {
        console.log(`  FAILED: ${step.name} (${(e as Error).message})`);
        this.compensate();
        return (this.outcome = `compensated at "${step.name}"`);
      }
    }
    return (this.outcome = "completed");
  }

  private compensate(): void {
    while (this.executed.length) {
      const { step, result } = this.executed.pop()!;
      console.log(`  undo: ${step.name} -> ${step.compensate(result)}`);
    }
  }
}

// Local services: each commits immediately, none knows the saga exists.
const stock: { reserved: string[] } = { reserved: [] };
const card: { charged: string[] } = { charged: [] };
const email = { failing: true };

const saga = new CheckoutSaga([
  {
    name: "reserve stock",
    action: () => {
      stock.reserved.push("sku-1");
      return "reservation r-1";
    },
    compensate: (r) => {
      stock.reserved.pop();
      return `${r} released`;
    },
  },
  {
    name: "charge card",
    action: () => {
      card.charged.push("c-1");
      return "receipt rc-1";
    },
    compensate: (r) => {
      card.charged.pop();
      return `${r} refunded`;
    },
  },
  {
    name: "send confirmation",
    action: () => {
      if (email.failing) throw new Error("mail queue down");
      return "sent";
    },
    compensate: () => "n/a",
  },
]);

console.log(saga.run()); // compensated at "send confirmation"
console.log(`stock: ${stock.reserved.length}, charges: ${card.charged.length}`);
// stock: 0, charges: 0 - every effect undone, in reverse order

In production the steps are remote calls: the compensations must be retried until they succeed (Retry), and each action carries an idempotency key so a compensation followed by a retry cannot double-charge (Idempotent Consumer).

Trade-offs

Gains

  • Multi-service workflows get all-or-nothing semantics without distributed locks.
  • Each service stays autonomous: it commits locally and exposes nothing but its own operations.
  • The compensation path is explicit, testable code - not an error message.

Costs

  • No isolation: concurrent sagas see intermediate states, so countermeasures (semantic locks, commutative updates) must be designed in.
  • Every step costs two implementations - the action and its compensation.
  • The failure path is the product: a saga whose compensations were never tested has not been written.
  • Orchestration centralizes logic; choreography scatters it - both are ongoing design decisions.

Relations

  • Transactional Outbox: how a step's own two writes - its local state change and the "tell the saga" message - commit atomically; the saga's reliability rests on it.
  • Idempotent Consumer: makes actions and compensations safe to re-run after crashes and at-least-once redelivery.
  • Retry: compensations must ultimately succeed, so they retry until they do - a compensation that gives up leaves the saga inconsistent.
  • Command: a step is a command with an undo; persisting commands as objects is what makes a crashed saga resumable.

Agent checklist

  • Every step has a named compensation that consumes the action's result; non-compensable steps are last or retriable.
  • Actions are idempotent or carry idempotency keys.
  • Saga state is persisted per step; a restart resumes, never repeats a side effect.
  • Compensations run in strict reverse order and continue even when one fails.
  • The terminal outcome is recorded and queryable.
  • Concurrent-saga interference has been considered, with a countermeasure or a written reason none is needed.

See also

Original writing and examples. Topic coverage follows microservices.io, which explains this topic with its own illustrations.