Marcelo Pastorino, Software Developer

Saga Pattern

Managing distributed transactions across services with a sequence of local transactions and compensating actions.

A saga is a sequence of local transactions that together implement a business process spanning multiple services. Each step commits in its own service and publishes an event or message that triggers the next step. When a step fails, the saga runs compensating transactions to undo the work already committed, restoring the system to a consistent state.

The problem it solves

A single ACID transaction cannot stretch across service boundaries. In a microservices system, placing an order might touch separate services for orders, payments, inventory, and shipping, each with its own database. A distributed transaction (two-phase commit) would couple them tightly, hold locks across the network, and hurt availability.

Sagas trade atomicity for eventual consistency: instead of one all-or-nothing transaction, you get a series of committed steps plus a defined way to roll them back semantically.

Compensating transactions

There is no automatic rollback once a local transaction commits. Each step that changes state needs a paired action that semantically reverses it:

  • Reserve inventory -> release inventory
  • Charge the card -> issue a refund
  • Create shipment -> cancel shipment

Compensations are business operations, not database rollbacks. A refund is a new transaction, not an undo, so the audit trail keeps both events.

Two coordination styles

Choreography

Each service listens for events and decides what to do next. There is no central coordinator; the workflow emerges from how services react to one another.

  • Pros: no single point of control, loose coupling, easy to add a participant.
  • Cons: the end-to-end flow is implicit and hard to trace; cyclic event dependencies can creep in.

Orchestration

A central orchestrator tells each service what to do and in what order, reacting to replies. The workflow lives in one place.

  • Pros: explicit, testable flow; centralized error handling and timeouts.
  • Cons: the orchestrator can grow into a bottleneck or god-object if it absorbs business logic.

A common rule of thumb: use choreography for short, simple flows and orchestration once a process has many steps or branching logic.

// Orchestrated saga with explicit compensations
async function placeOrderSaga(order) {
  const completed = [];

  try {
    await payments.charge(order);
    completed.push(() => payments.refund(order));

    await inventory.reserve(order);
    completed.push(() => inventory.release(order));

    await shipping.create(order);
    completed.push(() => shipping.cancel(order));

    await orders.markConfirmed(order);
  } catch (err) {
    // Run compensations in reverse order
    for (const compensate of completed.reverse()) {
      await compensate();
    }
    await orders.markFailed(order);
    throw err;
  }
}
// Orchestrated saga with explicit compensations
async function placeOrderSaga(order: Order): Promise<void> {
  const completed: Array<() => Promise<void>> = [];

  try {
    await payments.charge(order);
    completed.push(() => payments.refund(order));

    await inventory.reserve(order);
    completed.push(() => inventory.release(order));

    await shipping.create(order);
    completed.push(() => shipping.cancel(order));

    await orders.markConfirmed(order);
  } catch (err) {
    // Run compensations in reverse order
    for (const compensate of completed.reverse()) {
      await compensate();
    }
    await orders.markFailed(order);
    throw err;
  }
}
// Orchestrated saga with explicit compensations
public async Task PlaceOrderSagaAsync(Order order)
{
    var completed = new List<Func<Task>>();

    try
    {
        await _payments.ChargeAsync(order);
        completed.Add(() => _payments.RefundAsync(order));

        await _inventory.ReserveAsync(order);
        completed.Add(() => _inventory.ReleaseAsync(order));

        await _shipping.CreateAsync(order);
        completed.Add(() => _shipping.CancelAsync(order));

        await _orders.MarkConfirmedAsync(order);
    }
    catch
    {
        // Run compensations in reverse order
        completed.Reverse();
        foreach (var compensate in completed)
            await compensate();

        await _orders.MarkFailedAsync(order);
        throw;
    }
}
# Orchestrated saga with explicit compensations
async def place_order_saga(order: Order) -> None:
    completed: list[Callable[[], Awaitable[None]]] = []

    try:
        await payments.charge(order)
        completed.append(lambda: payments.refund(order))

        await inventory.reserve(order)
        completed.append(lambda: inventory.release(order))

        await shipping.create(order)
        completed.append(lambda: shipping.cancel(order))

        await orders.mark_confirmed(order)
    except Exception:
        # Run compensations in reverse order
        for compensate in reversed(completed):
            await compensate()
        await orders.mark_failed(order)
        raise

Reliability concerns

  • Idempotency: steps and compensations may be retried after a crash, so they must be safe to run more than once (see idempotency; dedupe on a saga or message ID).
  • Atomic publish: each step must commit its state change and emit its event together. Pair a saga with the outbox pattern to avoid losing the message that drives the next step.
  • Lost updates: because there is no global lock, concurrent sagas can interleave. Use semantic locks or status fields (for example, an order marked pending) to guard shared aggregates.
  • Failed compensations: a compensation can itself fail. Retry with backoff and surface a manual-intervention alert when it cannot complete.

Trade-offs

  • Replaces strong consistency with eventual consistency; clients may observe intermediate states.
  • Every state-changing step needs a designed, tested compensation.
  • Debugging spans multiple services; correlate logs with a shared saga ID.
  • Best reserved for genuinely cross-service workflows. A single-database process is simpler with one local transaction.

See event-driven architecture for the messaging model sagas build on, and the outbox pattern for publishing each step reliably.

See also

  • Event-Driven Architecture, Decoupling producers and consumers through asynchronous messaging.
  • Outbox Pattern, Reliably publishing events by committing them in the same transaction as state changes.
  • Eventual Consistency, A consistency model where replicas converge to the same state over time, allowing temporary divergence after a write.
  • Bounded Contexts, Aligning team language with explicit code and data boundaries.
  • Idempotency, Safe to run more than once without changing the outcome beyond the first successful apply.
  • Two-Phase Commit, A protocol that coordinates a single atomic transaction across multiple participants using a prepare phase and a commit phase.

On Wikipedia