# Distributed Transactions — Part 2: The Saga Pattern, Compensation, and Eventual Consistency

Part 1 covered why 2PC's blocking coordinator makes it a poor fit for typical microservice architectures. This part covers the pattern that replaced it in practice: the **Saga**, which trades strict atomicity for availability and eventual consistency.

## 1. The Core Idea: A Sequence of Local Transactions, Not One Distributed One

A Saga breaks one business operation into a sequence of **independent local transactions**, each fully committed on its own, one per service — with no cross-service locks held at any point, and no coordinator blocking anyone:

```
Order Saga:
  Step 1: Order Service   -> BEGIN; create order (status=PENDING); COMMIT.  (local, done)
  Step 2: Payment Service -> BEGIN; charge card; COMMIT.                    (local, done)
  Step 3: Inventory Service -> BEGIN; reserve stock; COMMIT.                (local, done)
  Step 4: Order Service   -> BEGIN; update order (status=CONFIRMED); COMMIT. (local, done)
```

Each step is a completely ordinary, fully-committed local transaction — the moment Step 1 commits, the order genuinely exists in the Order Service's database, visible to anything else querying it, with no lock held waiting for Step 2 or Step 3 to happen. This is the fundamental departure from 2PC: nothing is held in a "prepared but uncommitted" limbo state waiting on a coordinator's final word.

## 2. The Cost of That Freedom: Compensating Transactions

Committing each step independently means there's no way to "roll back" a later failure the way a single database transaction would — Step 1's commit is final and durable the instant it happens. If Step 3 (reserve stock) fails after Steps 1 and 2 have already committed, the Saga can't undo Step 2's card charge with a database `ROLLBACK` — that transaction is long since finished. Instead, the Saga must run an explicit **compensating transaction**: a new, separate operation whose entire purpose is to semantically undo the effect of an earlier step.

```
Order Saga, Step 3 (reserve stock) FAILS:
  Compensate Step 2: Payment Service -> BEGIN; refund the charge; COMMIT.
  Compensate Step 1: Order Service   -> BEGIN; update order (status=CANCELLED); COMMIT.
                                          (NOT delete the row -- see Section 4)
```

A compensating transaction is itself just an ordinary local transaction — but it's forward-moving business logic ("issue a refund," "cancel the order"), not a database-level undo. This means every step in a Saga that has a side effect worth compensating needs a corresponding compensating action **designed in advance**, as part of building the Saga, not something the database gives you for free.

![A Saga runs a sequence of independently-committed local transactions; if a later step fails, compensating transactions run in reverse order to semantically undo the already-committed earlier steps -- refund a charge, cancel a reservation -- rather than a database rollback](diagrams/01-saga-compensation.svg)

## 3. Two Ways to Coordinate a Saga: Choreography vs Orchestration

**Choreography** — no central coordinator; each service reacts to events published by the previous step and publishes its own event when done:

```
Order Service: creates order -> publishes OrderCreated
Payment Service: listens for OrderCreated -> charges card -> publishes PaymentCharged
Inventory Service: listens for PaymentCharged -> reserves stock -> publishes StockReserved
Order Service: listens for StockReserved -> confirms order

If Inventory Service instead publishes StockReservationFailed:
  Payment Service listens for THAT -> runs its refund compensation -> publishes PaymentRefunded
  Order Service listens for THAT -> cancels the order
```

Choreography keeps each service simple and fully decoupled (no service needs to know the whole saga's shape), but the overall flow of "what happens in what order" is implicit, scattered across every service's event handlers — understanding or debugging the full saga means tracing events through every participant's code.

**Orchestration** — a central orchestrator explicitly calls each step and explicitly triggers compensation on failure:

```ts
async function runOrderSaga(orderId: string) {
  await orderService.create(orderId);
  try {
    await paymentService.charge(orderId);
    try {
      await inventoryService.reserve(orderId);
    } catch {
      await paymentService.refund(orderId);       // compensate payment
      throw new Error("saga failed at inventory step");
    }
  } catch {
    await orderService.cancel(orderId);            // compensate order creation
    throw new Error("saga failed");
  }
  await orderService.confirm(orderId);
}
```

Orchestration makes the full sequence (and its compensation logic) explicit and readable in one place, at the cost of introducing a central component that every step depends on — not a *blocking* dependency like 2PC's coordinator (each step still commits independently and doesn't hold locks waiting on the orchestrator), but a component whose own reliability and correctness the whole saga now depends on.

## 4. Why Sagas Only Give You Eventual Consistency

Between Step 1 committing and Step 4 committing, the system is observably **inconsistent** from a strict standpoint — an order can exist with status PENDING while payment hasn't been charged yet, visible to anything querying the Order Service in that window. This is the fundamental tradeoff a Saga makes versus 2PC's all-or-nothing atomicity: a Saga guarantees the system will *eventually* reach a consistent end state (either fully confirmed, or fully compensated back to cancelled) — but not that every intermediate moment looks consistent to an outside observer. This is precisely why Step 1's compensation in Section 2 updates the order to CANCELLED rather than deleting it: other parts of the system may have already read and acted on the PENDING order in that intermediate window, and a visible, explicit CANCELLED state lets them react correctly, whereas a silent delete would leave them with no explanation for a customer-facing order that simply vanished.

## Common Pitfalls

1. **Forgetting that compensation is forward-moving logic, not a rollback** — a compensating transaction (refund, cancel, release) has to be designed and built for every compensable step; the database gives you none of this for free the way `ROLLBACK` does inside one transaction.
2. **Deleting/hard-reverting state during compensation instead of marking it explicitly failed/cancelled** — other parts of the system may have already observed and acted on the intermediate state; an explicit terminal status lets them react correctly instead of encountering data that silently vanished.
3. **Choreography sprawl** — as a saga grows more steps, purely event-driven choreography with no central view becomes very hard to trace end-to-end; teams often migrate a complex saga to orchestration specifically for that visibility.
4. **Assuming compensations always succeed** — a refund call can itself fail; production saga implementations need their OWN retry/backoff (previous topic) around compensating steps, and a plan for what happens if compensation itself can't complete.
5. **Expecting saga-based consistency to look like ACID from the outside** — code that reads state mid-saga (e.g., a PENDING order) needs to be written with that intermediate visibility in mind; treating a Saga's eventual consistency as if it were the same guarantee as a single ACID transaction leads to bugs where "impossible" intermediate states are observed and mishandled.

## Quick Reference

```
Saga = a sequence of independently-COMMITTED local transactions, one per service --
       no cross-service locks held, no blocking coordinator (contrast with 2PC)

On failure: run COMPENSATING transactions for already-committed steps, in reverse order
            (compensation = new forward-moving undo logic: refund, cancel, release --
             NOT a database rollback, since the original steps are already committed)

Choreography: services react to each other's events, no central coordinator -- simple per
              service, but the overall flow is implicit and scattered
Orchestration: a central orchestrator calls each step and its compensation explicitly --
               visible end-to-end flow, but a component the whole saga depends on

Guarantee: EVENTUAL consistency (will reach a consistent end state), NOT atomicity
           (intermediate states ARE observable to the rest of the system)
```
