# 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.

- kind: pattern
- category: data
- aliases: Compensating Transaction
- canonical: https://aipatterns.dev/design-patterns/saga
- inspired-by: https://microservices.io/patterns/data/saga.html

## 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](https://aipatterns.dev/design-patterns/command.md)-style records so a crashed process resumes instead of re-running side effects.

## Structure

```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](https://aipatterns.dev/design-patterns/idempotent-consumer.md)), 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.

```ts title="checkout.ts"
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
```

```python title="checkout.py"
from dataclasses import dataclass
from typing import Callable

@dataclass
class Step:
    name: str
    action: Callable[[], str]
    compensate: Callable[[str], str]

class CheckoutSaga:
    def __init__(self, steps: list[Step]) -> None:
        self._steps = steps
        self._executed: list[tuple[Step, str]] = []
        self.outcome = ""

    def run(self) -> str:
        for step in self._steps:
            try:
                result = step.action()
                self._executed.append((step, result))
                print(f"  ok: {step.name} -> {result}")
            except Exception as e:
                print(f"  FAILED: {step.name} ({e})")
                self._compensate()
                self.outcome = f'compensated at "{step.name}"'
                return self.outcome
        self.outcome = "completed"
        return self.outcome

    def _compensate(self) -> None:
        while self._executed:
            step, result = self._executed.pop()
            print(f"  undo: {step.name} -> {step.compensate(result)}")

# Local services: each commits immediately, none knows the saga exists.
stock: list[str] = []
card: list[str] = []
email_failing = True

def reserve() -> str:
    stock.append("sku-1")
    return "reservation r-1"

def release(reservation: str) -> str:
    stock.pop()
    return f"{reservation} released"

def charge() -> str:
    card.append("c-1")
    return "receipt rc-1"

def refund(receipt: str) -> str:
    card.pop()
    return f"{receipt} refunded"

def send() -> str:
    if email_failing:
        raise RuntimeError("mail queue down")
    return "sent"

saga = CheckoutSaga(
    [
        Step("reserve stock", reserve, release),
        Step("charge card", charge, refund),
        Step("send confirmation", send, lambda _: "n/a"),
    ]
)

print(saga.run())  # compensated at "send confirmation"
print(f"stock: {len(stock)}, charges: {len(card)}")
# stock: 0, charges: 0 - every effect undone, in reverse order
```

```go title="checkout.go"
package main

import "fmt"

type Step struct {
	Name       string
	Action     func() string
	Compensate func(result string) string
}

type executedStep struct {
	step   Step
	result string
}

type CheckoutSaga struct {
	steps    []Step
	executed []executedStep
	Outcome  string
}

func (s *CheckoutSaga) run() string {
	for _, step := range s.steps {
		result, err := safeCall(step.Action)
		if err != nil {
			fmt.Printf("  FAILED: %s (%v)\n", step.Name, err)
			s.compensate()
			s.Outcome = fmt.Sprintf("compensated at %q", step.Name)
			return s.Outcome
		}
		s.executed = append(s.executed, executedStep{step, result})
		fmt.Printf("  ok: %s -> %s\n", step.Name, result)
	}
	s.Outcome = "completed"
	return s.Outcome
}

func (s *CheckoutSaga) compensate() {
	for len(s.executed) > 0 {
		last := s.executed[len(s.executed)-1]
		s.executed = s.executed[:len(s.executed)-1]
		fmt.Printf("  undo: %s -> %s\n", last.step.Name, last.step.Compensate(last.result))
	}
}

func safeCall(action func() string) (result string, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("%v", r)
		}
	}()
	return action(), nil
}

func main() {
	var stock, card []string // local services: each commits immediately

	reserve := func() string {
		stock = append(stock, "sku-1")
		return "reservation r-1"
	}
	release := func(r string) string {
		stock = stock[:len(stock)-1]
		return r + " released"
	}
	charge := func() string {
		card = append(card, "c-1")
		return "receipt rc-1"
	}
	refund := func(r string) string {
		card = card[:len(card)-1]
		return r + " refunded"
	}
	emailFailing := true
	send := func() string {
		if emailFailing {
			panic("mail queue down")
		}
		return "sent"
	}

	saga := &CheckoutSaga{steps: []Step{
		{"reserve stock", reserve, release},
		{"charge card", charge, refund},
		{"send confirmation", send, func(string) string { return "n/a" }},
	}}

	fmt.Println(saga.run()) // compensated at "send confirmation"
	fmt.Printf("stock: %d, charges: %d\n", len(stock), len(card))
	// stock: 0, charges: 0 - every effect undone, in reverse order
}
```

```java title="Checkout.java"
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

public class Checkout {
    record Step(String name, Action action, Compensation compensate) {}

    interface Action {
        String run();
    }

    interface Compensation {
        String run(String result);
    }

    record Executed(Step step, String result) {}

    static final class CheckoutSaga {
        private final List<Step> steps;
        private final Deque<Executed> executed = new ArrayDeque<>();
        String outcome = "";

        CheckoutSaga(List<Step> steps) {
            this.steps = steps;
        }

        String run() {
            for (Step step : steps) {
                try {
                    String result = step.action().run();
                    executed.push(new Executed(step, result));
                    System.out.printf("  ok: %s -> %s%n", step.name(), result);
                } catch (RuntimeException e) {
                    System.out.printf("  FAILED: %s (%s)%n", step.name(), e.getMessage());
                    compensate();
                    outcome = "compensated at \"" + step.name() + "\"";
                    return outcome;
                }
            }
            outcome = "completed";
            return outcome;
        }

        private void compensate() {
            while (!executed.isEmpty()) {
                Executed done = executed.pop();
                System.out.printf("  undo: %s -> %s%n",
                        done.step().name(), done.step().compensate().run(done.result()));
            }
        }
    }

    public static void main(String[] args) {
        // Local services: each commits immediately, none knows the saga exists.
        List<String> stock = new ArrayList<>();
        List<String> card = new ArrayList<>();
        boolean emailFailing = true;

        Step reserve = new Step("reserve stock",
                () -> { stock.add("sku-1"); return "reservation r-1"; },
                r -> { stock.remove(stock.size() - 1); return r + " released"; });
        Step charge = new Step("charge card",
                () -> { card.add("c-1"); return "receipt rc-1"; },
                r -> { card.remove(card.size() - 1); return r + " refunded"; });
        Step send = new Step("send confirmation",
                () -> { if (emailFailing) throw new IllegalStateException("mail queue down"); return "sent"; },
                r -> "n/a");

        CheckoutSaga saga = new CheckoutSaga(List.of(reserve, charge, send));
        System.out.println(saga.run()); // compensated at "send confirmation"
        System.out.printf("stock: %d, charges: %d%n", stock.size(), card.size());
        // 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](https://aipatterns.dev/design-patterns/retry.md)), and each action carries an idempotency key so a compensation followed by a retry cannot double-charge ([Idempotent Consumer](https://aipatterns.dev/design-patterns/idempotent-consumer.md)).

## 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](https://aipatterns.dev/design-patterns/transactional-outbox.md): 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](https://aipatterns.dev/design-patterns/idempotent-consumer.md): makes actions and compensations safe to re-run after crashes and at-least-once redelivery.
- [Retry](https://aipatterns.dev/design-patterns/retry.md): compensations must ultimately succeed, so they retry until they do - a compensation that gives up leaves the saga inconsistent.
- [Command](https://aipatterns.dev/design-patterns/command.md): 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.
