# Saga Pattern

## Problem

In a distributed system, a business transaction may span multiple services. Traditional ACID transactions don't work across service boundaries. If step 3 of a 5-step process fails, the completed steps 1 and 2 need to be undone — but you can't just "rollback" because each service manages its own database.

## Solution

A saga is a sequence of local transactions where each step has a **compensating action** that can semantically undo its effect. There are two coordination approaches:

- **Orchestration** — A central coordinator tells each service what to do and handles compensation on failure.
- **Choreography** — Each service listens for events and decides locally what to do next, publishing its own events.

## When to Use

- Business transactions that span multiple microservices
- Long-running workflows (order fulfillment, onboarding)
- When distributed transactions (2PC) are too expensive or unavailable
- When eventual consistency is acceptable

## When to Avoid

- Single-service operations that can use local transactions
- When strong consistency is absolutely required (use 2PC)
- Simple CRUD operations with no cross-service concerns
- When the compensation logic is impossible to implement

## Pseudocode

```
# Orchestration approach
class SagaOrchestrator:
    steps = [(action1, compensate1), (action2, compensate2), ...]
    completed = []

    function execute():
        for (action, compensation) in steps:
            try:
                result = action()
                completed.push((compensation, result))
            catch error:
                rollback()
                raise SagaFailed

    function rollback():
        for (compensation, result) in reversed(completed):
            compensation(result)
```

## Python Implementation

```python
from typing import Callable, Any


class SagaFailed(Exception):
    def __init__(self, step: str, cause: str, compensated: list[str]):
        self.step = step
        self.cause = cause
        self.compensated = compensated
        super().__init__(
            f"Saga failed at '{step}': {cause}. "
            f"Compensated: {compensated}"
        )


class SagaStep:
    def __init__(self, name: str, action: Callable, compensation: Callable):
        self.name = name
        self.action = action
        self.compensation = compensation


class SagaOrchestrator:
    def __init__(self, name: str):
        self.name = name
        self.steps: list[SagaStep] = []

    def add_step(self, name: str, action: Callable, compensation: Callable):
        self.steps.append(SagaStep(name, action, compensation))
        return self

    def execute(self, context: dict[str, Any]) -> dict[str, Any]:
        completed: list[SagaStep] = []
        print(f"[Saga:{self.name}] Starting...")

        for step in self.steps:
            try:
                print(f"  -> Executing '{step.name}'")
                step.action(context)
                completed.append(step)
            except Exception as exc:
                error_msg = str(exc)
                print(f"  !! Failed at '{step.name}': {error_msg}")
                compensated = self._rollback(completed, context)
                raise SagaFailed(step.name, error_msg, compensated)

        print(f"[Saga:{self.name}] Completed successfully.")
        return context

    def _rollback(self, completed: list[SagaStep], context: dict) -> list[str]:
        compensated = []
        for step in reversed(completed):
            try:
                print(f"  <- Compensating '{step.name}'")
                step.compensation(context)
                compensated.append(step.name)
            except Exception as exc:
                print(f"  !! Compensation failed for '{step.name}': {exc}")
        return compensated


# --- Demo: Order processing saga ---

def reserve_inventory(ctx):
    ctx["inventory_reserved"] = True
    print(f"     Reserved {ctx['quantity']}x {ctx['item']}")

def release_inventory(ctx):
    ctx["inventory_reserved"] = False
    print(f"     Released {ctx['quantity']}x {ctx['item']}")

def charge_payment(ctx):
    ctx["payment_charged"] = True
    ctx["charge_id"] = "CHG-12345"
    print(f"     Charged ${ctx['amount']} (ref: {ctx['charge_id']})")

def refund_payment(ctx):
    ctx["payment_charged"] = False
    print(f"     Refunded ${ctx['amount']} (ref: {ctx['charge_id']})")

def ship_order(ctx):
    if ctx.get("force_ship_fail"):
        raise RuntimeError("Warehouse system offline")
    ctx["shipped"] = True
    print(f"     Shipped order to {ctx['address']}")

def cancel_shipment(ctx):
    ctx["shipped"] = False
    print(f"     Cancelled shipment")


saga = SagaOrchestrator("OrderProcessing")
saga.add_step("reserve_inventory", reserve_inventory, release_inventory)
saga.add_step("charge_payment", charge_payment, refund_payment)
saga.add_step("ship_order", ship_order, cancel_shipment)

# Successful run
print("=== Successful Saga ===")
ctx1 = {"item": "Widget", "quantity": 3, "amount": 29.99, "address": "123 Main St"}
saga.execute(ctx1)
print(f"  Final state: shipped={ctx1['shipped']}, charged={ctx1['payment_charged']}")

print()

# Failing run — shipping fails, triggers compensation
print("=== Failing Saga (ship fails) ===")
ctx2 = {"item": "Gadget", "quantity": 1, "amount": 49.99, "address": "456 Oak Ave",
        "force_ship_fail": True}
try:
    saga.execute(ctx2)
except SagaFailed as e:
    print(f"  Saga error: {e}")
    print(f"  Final state: inventory_reserved={ctx2.get('inventory_reserved')}, "
          f"payment_charged={ctx2.get('payment_charged')}")
```

**Output:**
```
=== Successful Saga ===
[Saga:OrderProcessing] Starting...
  -> Executing 'reserve_inventory'
     Reserved 3x Widget
  -> Executing 'charge_payment'
     Charged $29.99 (ref: CHG-12345)
  -> Executing 'ship_order'
     Shipped order to 123 Main St
[Saga:OrderProcessing] Completed successfully.
  Final state: shipped=True, charged=True

=== Failing Saga (ship fails) ===
[Saga:OrderProcessing] Starting...
  -> Executing 'reserve_inventory'
     Reserved 1x Gadget
  -> Executing 'charge_payment'
     Charged $49.99 (ref: CHG-12345)
  -> Executing 'ship_order'
  !! Failed at 'ship_order': Warehouse system offline
  <- Compensating 'charge_payment'
     Refunded $49.99 (ref: CHG-12345)
  <- Compensating 'reserve_inventory'
     Released 1x Gadget
  Saga error: Saga failed at 'ship_order': Warehouse system offline. Compensated: ['charge_payment', 'reserve_inventory']
  Final state: inventory_reserved=False, payment_charged=False
```

## JavaScript Implementation

```javascript
class SagaFailed extends Error {
  constructor(step, cause, compensated) {
    super(`Saga failed at '${step}': ${cause}. Compensated: [${compensated.join(", ")}]`);
    this.step = step;
    this.cause = cause;
    this.compensated = compensated;
  }
}

class SagaOrchestrator {
  constructor(name) {
    this.name = name;
    this.steps = [];
  }

  addStep(name, action, compensation) {
    this.steps.push({ name, action, compensation });
    return this;
  }

  async execute(context) {
    const completed = [];
    console.log(`[Saga:${this.name}] Starting...`);

    for (const step of this.steps) {
      try {
        console.log(`  -> Executing '${step.name}'`);
        await step.action(context);
        completed.push(step);
      } catch (err) {
        console.log(`  !! Failed at '${step.name}': ${err.message}`);
        const compensated = await this._rollback(completed, context);
        throw new SagaFailed(step.name, err.message, compensated);
      }
    }

    console.log(`[Saga:${this.name}] Completed successfully.`);
    return context;
  }

  async _rollback(completed, context) {
    const compensated = [];
    for (const step of [...completed].reverse()) {
      try {
        console.log(`  <- Compensating '${step.name}'`);
        await step.compensation(context);
        compensated.push(step.name);
      } catch (err) {
        console.log(`  !! Compensation failed for '${step.name}': ${err.message}`);
      }
    }
    return compensated;
  }
}

// --- Demo ---
const saga = new SagaOrchestrator("OrderProcessing");

saga.addStep("reserve_inventory",
  (ctx) => { ctx.inventoryReserved = true; console.log(`     Reserved ${ctx.quantity}x ${ctx.item}`); },
  (ctx) => { ctx.inventoryReserved = false; console.log(`     Released ${ctx.quantity}x ${ctx.item}`); }
);
saga.addStep("charge_payment",
  (ctx) => { ctx.paymentCharged = true; ctx.chargeId = "CHG-12345"; console.log(`     Charged $${ctx.amount} (ref: ${ctx.chargeId})`); },
  (ctx) => { ctx.paymentCharged = false; console.log(`     Refunded $${ctx.amount} (ref: ${ctx.chargeId})`); }
);
saga.addStep("ship_order",
  (ctx) => { if (ctx.forceShipFail) throw new Error("Warehouse system offline"); ctx.shipped = true; console.log(`     Shipped to ${ctx.address}`); },
  (ctx) => { ctx.shipped = false; console.log("     Cancelled shipment"); }
);

(async () => {
  console.log("=== Successful Saga ===");
  const ctx1 = { item: "Widget", quantity: 3, amount: 29.99, address: "123 Main St" };
  await saga.execute(ctx1);
  console.log(`  Final state: shipped=${ctx1.shipped}, charged=${ctx1.paymentCharged}\n`);

  console.log("=== Failing Saga (ship fails) ===");
  const ctx2 = { item: "Gadget", quantity: 1, amount: 49.99, address: "456 Oak Ave", forceShipFail: true };
  try {
    await saga.execute(ctx2);
  } catch (e) {
    console.log(`  Saga error: ${e.message}`);
    console.log(`  Final state: inventoryReserved=${ctx2.inventoryReserved}, paymentCharged=${ctx2.paymentCharged}`);
  }
})();
```

## Related Patterns

- **Command** — Each Saga step is a Command (with its compensating command for rollback).
- **Observer** — Choreography-based Sagas use events (Observer/Pub-Sub) to trigger each step.
- **Mediator** — Orchestration-based Sagas use a central Orchestrator (Mediator) that calls each step.
- **Dead Letter Queue** — Failed Saga steps should route to DLQ for inspection and manual retry.
- **Unit of Work** — Within a single service, a Saga step uses Unit of Work for atomic DB changes.
