Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

System Design (High-Level Design)  Module 11 – Distributed transactions and workflows

Sagas: orchestration, choreography, compensation

How a saga splits a business transaction into local steps with compensations, when to orchestrate or choreograph it, and where the pivot goes.

  • Advanced
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free core, Pro depth: 9 of 25 minutes free
Free: A saga commits each step and undoes it with a compensation · Three kinds of step decide where the pivot goes · Orchestration keeps the flow in one place; choreography spreads it · Practise: plan the recovery. With a pass: 2 in-depth parts and 3 interview answers, about 16 minutes.

What you will learn

  • Model a multi-service business transaction as a saga of local steps and compensations
  • Choose orchestration or choreography for a saga and justify the choice
  • Design compensations and place the pivot step
  • Prevent duplicate effects when saga steps and compensations are repeated after a crash

Before you start

What you’ll learn · Before you start

What you will learn

  • Model a multi-service business transaction as a saga of local steps and compensations
  • Choose orchestration or choreography for a saga and justify the choice
  • Design compensations and place the pivot step
  • Prevent duplicate effects when saga steps and compensations are repeated after a crash

Before you start

On this page

An online order touches four services, and the card is declined after the stock has been reserved: two of its six steps have already committed in other databases, and nothing can roll them back. A saga: A business transaction across several services, run as a sequence of local transactions, each paired with a compensating transaction that undoes its effect if a later step fails. carries out such a business transaction as a sequence of local transactions, each committed in one service’s own database, and pairs every step with a compensating transaction: A new transaction with the opposite business effect of a step that has already committed, such as releasing a reservation or issuing a refund. It does not restore old values; it leaves the business in an acceptable state. that undoes its effect if a later step fails. Either every step runs, or the steps that ran are followed by their compensations in reverse order.

Nothing holds locks across services, so a saga never blocks the way two-phase commit can. The price is that other requests can see its half-finished state, and that an undo is a new action, a refund or a cancellation, not a rollback. The steps are coordinated in one of two ways: orchestration (saga): Running a saga from one coordinator that keeps the saga's state, sends each service a command and decides the next step from the reply., where one coordinator tells each service what to do, or choreography (saga): Running a saga without a coordinator: each service reacts to the events of the step before and publishes its own, so the flow is the sum of the services' subscriptions., where each service reacts to the events of the one before.

A saga commits each step and undoes it with a compensation

Garcia-Molina and Salem named the idea in 1987 for long-lived transactions that would otherwise hold locks for hours (Garcia-Molina and Salem). An online order that touches orders, stock, payments and shipping becomes six local transactions:

An order saga of six steps: three steps with undo actions, capture payment as the pivot, then two steps that are retried, never undone.Up to the pivot:a failure undoes everystep done, newest firstAfter the pivot:a failure is retried,never undone1. Create orderundo: cancel the order2. Reserve stockundo: release the stock3. Authorise payment(a hold)undo: void the authorisation4. Capture payment:THE PIVOTonce it succeeds,there is no way back5. Book courierretried until it succeeds6. Send confirmationretried until it succeeds

The order saga: steps, compensations and the pivot

Text description of the diagram

The diagram has two boxes, one above the other, and the steps are numbered in the order they run.

The upper box is labelled "Up to the pivot: a failure undoes every step done, newest first". It holds four steps:

  1. Create order; undo: cancel the order.
  2. Reserve stock; undo: release the stock.
  3. Authorise payment, which places a hold; undo: void the authorisation.
  4. Capture payment, the pivot: once it succeeds, there is no way back.

The lower box is labelled "After the pivot: a failure is retried, never undone". It holds two steps:

  1. Book courier, retried until it succeeds.
  2. Send confirmation, retried until it succeeds.

Step through the order of the opening, the one whose card is declined at step 3:

Interactive · step by step The order saga when the card is declined Step through the order saga for one order. Each step commits in its own service; when step 3 fails, the steps that ran are undone, newest first.
The order saga when the card is declined, step 6 of 6. Showing 1. Create order (undone: CANCELLED), 2. Reserve stock (undone: stock released), 3. Authorise payment (failed: card declined), 4. Capture: the pivot, 5. Book courier, 6. Send confirmation. In focus: 4. Capture: the pivot, 5. Book courier, 6. Send confirmation.1. Create orderundone: CANCELLED2. Reserve stockundone: stock released3. Authorise paymentfailed: card declined4. Capture: the pivotnot run yet5. Book couriernot run yet6. Send confirmationnot run yet

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Create the order. The order service commits order 1001 as PENDING in its own database. Anyone who reads it now sees a pending order.
  2. Reserve the stock. The stock service commits a reservation of one unit. Other buyers see one unit fewer at once, long before the payment.
  3. The card is declined. The payment service refuses the authorisation, so step 3 commits nothing. Steps 1 and 2 are already committed in other databases, and nothing can roll them back.
  4. Undo step 2 first. The saga runs the compensations of the steps that succeeded, newest first. Releasing the reservation puts the unit back for other buyers.
  5. Then undo step 1. Cancelling the order is a new transaction with the opposite business effect, so the order row stays, as CANCELLED, instead of disappearing.
  6. The pivot was never reached. Steps 4 to 6 never ran, so the card was never charged and there is nothing to refund. The saga ends with every step either undone or never started.

Each step commits on its own and is visible at once: other buyers see the reserved unit gone before the payment step has started. When the card is declined, nothing is rolled back; the saga runs the compensations of the steps that succeeded, newest first, releasing the stock and then cancelling the order. The paper’s own example is an airline booking whose compensation cancels a seat by subtracting one from the reservations count instead of writing back the old count, because other bookings may have changed it in the meantime.

Checkpoint

Check: Local steps and compensations

  1. Question 1 of 2 In the order saga, the stock was reserved and then the card was declined. What does the saga run next?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Release the stock, then cancel the order

    The steps that succeeded are already committed in their own databases, so nothing can be rolled back. Their compensations run instead, newest first: the stock release undoes step 2, then cancelling the order undoes step 1.

  2. Question 2 of 2 What does a compensating transaction promise?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: To undo the step's effect in business terms, even if the data does not return to its earlier state

    A compensation is a new action with the opposite business effect, such as releasing one reserved unit. It leaves the business in an acceptable state rather than restoring old values.

Three kinds of step decide where the pivot goes

Azure’s description of the pattern sorts saga steps into three kinds (Azure):

  • A compensable step has an undo: a later transaction that cancels its business effect.
  • The pivot (saga): The step of a saga after which it can no longer turn back, such as capturing a payment. The steps before it can be compensated; the steps after it are retried until they succeed. is the point of no return: once it succeeds, the saga must finish.
  • A retryable step comes after the pivot and is idempotent, so it is repeated until it succeeds.

The rule that follows is about order. Put the steps most likely to fail before the pivot, where they cost a cheap compensation, and put the irreversible step, here capturing the payment, after every check that might reject the order. A saga that charges the card first turns every out-of-stock item into a refund. And remember that compensation is not rollback: a sent email stays sent, and the best a compensation can do is send a correction.

The stepper runs the order saga once with no failure and once for each step that has a failure reason in its table, and prints every step and compensation. Give another step a reason, or clear one, and run it again:

One program · 14 languages The saga stepper, one run per failing step

Python 3.14 · main.py

SAGA = [  # (step, kind, compensation, why a run fails at this step)
    ("create order", "compensable", "cancel order", ""),
    ("reserve stock", "compensable", "release stock", "out of stock"),
    ("authorise payment", "compensable", "void the authorisation", "card declined"),
    ("capture payment", "pivot", "", "capture refused"),
    ("book courier", "retryable", "", "courier unavailable"),
    ("send confirmation", "retryable", "", ""),
]


def run(failing: str) -> str:
    done: list[str] = []
    for step, kind, compensation, why in SAGA:
        if step == failing and kind != "retryable":
            print(f"  failed  {step}: {why}")
            for undo in reversed(done):  # compensations run newest first
                print(f"  undo    {undo}")
            return "CANCELLED"
        if step == failing:  # after the pivot: retried until it works
            for attempt in (1, 2):
                print(f"  retry   {step}: attempt {attempt} failed ({why})")
        print(
            f"  done    {step}" + (" (pivot: no way back)" if kind == "pivot" else "")
        )
        if compensation:
            done.append(compensation)
    return "CONFIRMED"


print("One run per failing step")
for failing in ["", *(step for step, _, _, why in SAGA if why)]:
    print()
    print(f"'{failing}' fails" if failing else "No step fails")
    print(f"  result: order {run(failing)}")

In Python: The last column of each step says why a run fails there, so the runs to make are the steps with a reason; reversed(done) walks the compensations newest first without copying the list.

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

const saga = [
  // [step, kind, compensation, why a run fails at this step]
  ["create order", "compensable", "cancel order", ""],
  ["reserve stock", "compensable", "release stock", "out of stock"],
  ["authorise payment", "compensable", "void the authorisation", "card declined"],
  ["capture payment", "pivot", "", "capture refused"],
  ["book courier", "retryable", "", "courier unavailable"],
  ["send confirmation", "retryable", "", ""],
];

function run(failing) {
  const done = [];
  for (const [step, kind, compensation, why] of saga) {
    if (step === failing && kind !== "retryable") {
      console.log(`  failed  ${step}: ${why}`);
      // compensations run newest first
      for (const undo of [...done].reverse()) console.log(`  undo    ${undo}`);
      return "CANCELLED";
    }
    if (step === failing) {
      // after the pivot: retried until it works
      for (const attempt of [1, 2])
        console.log(`  retry   ${step}: attempt ${attempt} failed (${why})`);
    }
    console.log(`  done    ${step}${kind === "pivot" ? " (pivot: no way back)" : ""}`);
    if (compensation) done.push(compensation);
  }
  return "CONFIRMED";
}

console.log("One run per failing step");
for (const failing of ["", ...saga.filter(([, , , why]) => why).map(([step]) => step)]) {
  console.log("");
  console.log(failing ? `'${failing}' fails` : "No step fails");
  console.log(`  result: order ${run(failing)}`);
}

In JavaScript: saga.filter keeps the steps that have a failure reason, in saga order, and [...done].reverse() reverses a copy, so done itself is never changed.

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

import java.util.ArrayList;
import java.util.List;

public class Main {
  record Step(String name, String kind, String compensation, String why) {}

  // why: why a run fails at this step
  static final List<Step> SAGA =
      List.of(
          new Step("create order", "compensable", "cancel order", ""),
          new Step("reserve stock", "compensable", "release stock", "out of stock"),
          new Step("authorise payment", "compensable", "void the authorisation", "card declined"),
          new Step("capture payment", "pivot", "", "capture refused"),
          new Step("book courier", "retryable", "", "courier unavailable"),
          new Step("send confirmation", "retryable", "", ""));

  static String run(String failing) {
    List<String> done = new ArrayList<>();
    for (Step step : SAGA) {
      boolean failed = step.name().equals(failing);
      if (failed && !step.kind().equals("retryable")) {
        System.out.println("  failed  " + step.name() + ": " + step.why());
        // compensations run newest first
        for (int i = done.size() - 1; i >= 0; i--) {
          System.out.println("  undo    " + done.get(i));
        }
        return "CANCELLED";
      }
      if (failed) {
        // after the pivot: retried until it works
        for (int attempt = 1; attempt <= 2; attempt++) {
          System.out.println(
              "  retry   " + step.name() + ": attempt " + attempt + " failed (" + step.why() + ")");
        }
      }
      String pivot = step.kind().equals("pivot") ? " (pivot: no way back)" : "";
      System.out.println("  done    " + step.name() + pivot);
      if (!step.compensation().isEmpty()) {
        done.add(step.compensation());
      }
    }
    return "CONFIRMED";
  }

  public static void main(String[] args) {
    System.out.println("One run per failing step");
    List<String> cases = new ArrayList<>(List.of(""));
    SAGA.stream().filter(s -> !s.why().isEmpty()).forEach(s -> cases.add(s.name()));
    for (String failing : cases) {
      System.out.println();
      System.out.println(failing.isEmpty() ? "No step fails" : "'" + failing + "' fails");
      System.out.println("  result: order " + run(failing));
    }
  }
}

In Java: Each step is a record, and a stream collects the failing runs; the compensations are read backwards by index, which also compiles on Java 17.

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <print>
#include <string_view>
#include <vector>

enum class Kind { compensable, pivot, retryable };

struct Step {
  std::string_view name;
  Kind kind;
  std::string_view compensation;
  std::string_view why; // why a run fails at this step
};

constexpr Step SAGA[] = {
    {"create order", Kind::compensable, "cancel order", ""},
    {"reserve stock", Kind::compensable, "release stock", "out of stock"},
    {"authorise payment", Kind::compensable, "void the authorisation",
     "card declined"},
    {"capture payment", Kind::pivot, "", "capture refused"},
    {"book courier", Kind::retryable, "", "courier unavailable"},
    {"send confirmation", Kind::retryable, "", ""}};

std::string_view run(std::string_view failing) {
  std::vector<std::string_view> done;
  for (const Step &step : SAGA) {
    const bool failed = step.name == failing;
    if (failed && step.kind != Kind::retryable) {
      std::println("  failed  {}: {}", step.name, step.why);
      // compensations run newest first
      for (auto undo = done.rbegin(); undo != done.rend(); ++undo) {
        std::println("  undo    {}", *undo);
      }
      return "CANCELLED";
    }
    if (failed) { // after the pivot: retried until it works
      for (int attempt = 1; attempt <= 2; attempt++) {
        std::println("  retry   {}: attempt {} failed ({})", step.name, attempt,
                     step.why);
      }
    }
    const bool pivot = step.kind == Kind::pivot;
    std::println("  done    {}{}", step.name,
                 pivot ? " (pivot: no way back)" : "");
    if (!step.compensation.empty()) {
      done.push_back(step.compensation);
    }
  }
  return "CONFIRMED";
}

int main() {
  std::println("One run per failing step\n\nNo step fails");
  std::println("  result: order {}", run(""));
  for (const Step &step : SAGA) {
    if (!step.why.empty()) {
      std::println("\n'{}' fails", step.name);
      std::println("  result: order {}", run(step.name));
    }
  }
}

In C++: done holds string_views into the constexpr SAGA table, and rbegin() to rend() walks it backwards, newest compensation first.

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

// Why: why a run fails at this step.
Step[] saga =
[
    new("create order", "compensable", "cancel order", ""),
    new("reserve stock", "compensable", "release stock", "out of stock"),
    new("authorise payment", "compensable", "void the authorisation", "card declined"),
    new("capture payment", "pivot", "", "capture refused"),
    new("book courier", "retryable", "", "courier unavailable"),
    new("send confirmation", "retryable", "", ""),
];

Console.WriteLine("One run per failing step");
foreach (string failing in saga.Where(s => s.Why != "").Select(s => s.Name).Prepend(""))
{
    Console.WriteLine();
    Console.WriteLine(failing == "" ? "No step fails" : $"'{failing}' fails");
    Console.WriteLine($"  result: order {Run(saga, failing)}");
}

static string Run(Step[] saga, string failing)
{
    var done = new List<string>();
    foreach (var (step, kind, compensation, why) in saga)
    {
        if (step == failing && kind != "retryable")
        {
            Console.WriteLine($"  failed  {step}: {why}");
            // compensations run newest first
            for (int i = done.Count - 1; i >= 0; i--)
            {
                Console.WriteLine($"  undo    {done[i]}");
            }
            return "CANCELLED";
        }
        if (step == failing)
        {
            // after the pivot: retried until it works
            for (int attempt = 1; attempt <= 2; attempt++)
            {
                Console.WriteLine($"  retry   {step}: attempt {attempt} failed ({why})");
            }
        }
        Console.WriteLine($"  done    {step}{(kind == "pivot" ? " (pivot: no way back)" : "")}");
        if (compensation != "")
        {
            done.Add(compensation);
        }
    }
    return "CONFIRMED";
}

record Step(string Name, string Kind, string Compensation, string Why);

In C#: Step is a positional record that foreach deconstructs, and Where, Select and Prepend("") put the run with no failure before the failing ones.

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

#include <stdio.h>
#include <string.h>

enum kind { COMPENSABLE, PIVOT, RETRYABLE };

struct step {
  const char *name;
  enum kind kind;
  const char *compensation;
  const char *why; // why a run fails at this step
};

static const struct step SAGA[] = {
    {"create order", COMPENSABLE, "cancel order", ""},
    {"reserve stock", COMPENSABLE, "release stock", "out of stock"},
    {"authorise payment", COMPENSABLE, "void the authorisation",
     "card declined"},
    {"capture payment", PIVOT, "", "capture refused"},
    {"book courier", RETRYABLE, "", "courier unavailable"},
    {"send confirmation", RETRYABLE, "", ""},
};
enum { STEPS = sizeof SAGA / sizeof SAGA[0] };

static const char *run(const char *failing) {
  const char *done[STEPS];
  int count = 0;
  for (int i = 0; i < STEPS; i++) {
    const struct step *step = &SAGA[i];
    const bool failed = strcmp(step->name, failing) == 0;
    if (failed && step->kind != RETRYABLE) {
      printf("  failed  %s: %s\n", step->name, step->why);
      // compensations run newest first
      while (count > 0) {
        printf("  undo    %s\n", done[--count]);
      }
      return "CANCELLED";
    }
    if (failed) { // after the pivot: retried until it works
      for (int attempt = 1; attempt <= 2; attempt++) {
        printf("  retry   %s: attempt %d failed (%s)\n", step->name, attempt,
               step->why);
      }
    }
    printf("  done    %s%s\n", step->name,
           step->kind == PIVOT ? " (pivot: no way back)" : "");
    if (step->compensation[0] != '\0') {
      done[count++] = step->compensation;
    }
  }
  return "CONFIRMED";
}

int main(void) {
  printf("One run per failing step\n\nNo step fails\n");
  printf("  result: order %s\n", run(""));
  for (int i = 0; i < STEPS; i++) {
    if (SAGA[i].why[0] != '\0') {
      printf("\n'%s' fails\n", SAGA[i].name);
      printf("  result: order %s\n", run(SAGA[i].name));
    }
  }
}

In C: done is a stack of pointers into the static SAGA table: a compensation is pushed with done[count++] and popped by the undo loop with done[--count].

Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main

PHP 8.5 · main.php

<?php

declare(strict_types=1);

/** Each step: [name, kind, compensation, why a run fails at this step] */
const SAGA = [
    ['create order', 'compensable', 'cancel order', ''],
    ['reserve stock', 'compensable', 'release stock', 'out of stock'],
    ['authorise payment', 'compensable', 'void the authorisation', 'card declined'],
    ['capture payment', 'pivot', '', 'capture refused'],
    ['book courier', 'retryable', '', 'courier unavailable'],
    ['send confirmation', 'retryable', '', ''],
];

function run(string $failing): string
{
    $done = [];
    foreach (SAGA as [$step, $kind, $compensation, $why]) {
        if ($step === $failing && $kind !== 'retryable') {
            echo "  failed  {$step}: {$why}\n";
            // compensations run newest first
            foreach (array_reverse($done) as $undo) {
                echo "  undo    {$undo}\n";
            }
            return 'CANCELLED';
        }
        if ($step === $failing) {
            // after the pivot: retried until it works
            foreach ([1, 2] as $attempt) {
                echo "  retry   {$step}: attempt {$attempt} failed ({$why})\n";
            }
        }
        echo "  done    {$step}", $kind === 'pivot' ? ' (pivot: no way back)' : '', "\n";
        if ($compensation !== '') {
            $done[] = $compensation;
        }
    }
    return 'CONFIRMED';
}

echo "One run per failing step\n";
$failing = array_column(array_filter(SAGA, fn(array $step): bool => $step[3] !== ''), 0);
foreach (['', ...$failing] as $name) {
    echo "\n", $name === '' ? 'No step fails' : "'{$name}' fails", "\n";
    $result = run($name);
    echo "  result: order {$result}\n";
}

In PHP: array_filter keeps the steps that have a failure reason and array_column takes their names; each step unpacks as [$step, $kind, $compensation, $why].

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

#[derive(PartialEq)]
enum Kind {
    Compensable,
    Pivot,
    Retryable,
}

// (step, kind, compensation, why a run fails at this step)
const SAGA: [(&str, Kind, &str, &str); 6] = [
    ("create order", Kind::Compensable, "cancel order", ""),
    (
        "reserve stock",
        Kind::Compensable,
        "release stock",
        "out of stock",
    ),
    (
        "authorise payment",
        Kind::Compensable,
        "void the authorisation",
        "card declined",
    ),
    ("capture payment", Kind::Pivot, "", "capture refused"),
    ("book courier", Kind::Retryable, "", "courier unavailable"),
    ("send confirmation", Kind::Retryable, "", ""),
];

fn run(failing: &str) -> &'static str {
    let mut done = Vec::new();
    for (step, kind, compensation, why) in &SAGA {
        if *step == failing && *kind != Kind::Retryable {
            println!("  failed  {step}: {why}");
            // compensations run newest first
            for undo in done.iter().rev() {
                println!("  undo    {undo}");
            }
            return "CANCELLED";
        }
        if *step == failing {
            // after the pivot: retried until it works
            for attempt in 1..=2 {
                println!("  retry   {step}: attempt {attempt} failed ({why})");
            }
        }
        let pivot = if *kind == Kind::Pivot {
            " (pivot: no way back)"
        } else {
            ""
        };
        println!("  done    {step}{pivot}");
        if !compensation.is_empty() {
            done.push(*compensation);
        }
    }
    "CONFIRMED"
}

fn main() {
    println!("One run per failing step");
    let failing = SAGA
        .iter()
        .filter(|step| !step.3.is_empty())
        .map(|step| step.0);
    for name in std::iter::once("").chain(failing) {
        println!();
        if name.is_empty() {
            println!("No step fails");
        } else {
            println!("'{name}' fails");
        }
        println!("  result: order {}", run(name));
    }
}

In Rust: iter().rev() walks done backwards without copying it, and the failing runs are an iterator chained after the run with no failure.

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

type Kind = "compensable" | "pivot" | "retryable";

// [step, kind, compensation, why a run fails at this step]
const saga: [string, Kind, string, string][] = [
  ["create order", "compensable", "cancel order", ""],
  ["reserve stock", "compensable", "release stock", "out of stock"],
  ["authorise payment", "compensable", "void the authorisation", "card declined"],
  ["capture payment", "pivot", "", "capture refused"],
  ["book courier", "retryable", "", "courier unavailable"],
  ["send confirmation", "retryable", "", ""],
];

function run(failing: string): string {
  const done: string[] = [];
  for (const [step, kind, compensation, why] of saga) {
    if (step === failing && kind !== "retryable") {
      console.log(`  failed  ${step}: ${why}`);
      // compensations run newest first
      for (const undo of [...done].reverse()) console.log(`  undo    ${undo}`);
      return "CANCELLED";
    }
    if (step === failing) {
      // after the pivot: retried until it works
      for (const attempt of [1, 2])
        console.log(`  retry   ${step}: attempt ${attempt} failed (${why})`);
    }
    console.log(`  done    ${step}${kind === "pivot" ? " (pivot: no way back)" : ""}`);
    if (compensation) done.push(compensation);
  }
  return "CONFIRMED";
}

console.log("One run per failing step");
for (const failing of ["", ...saga.filter(([, , , why]) => why).map(([step]) => step)]) {
  console.log("");
  console.log(failing ? `'${failing}' fails` : "No step fails");
  console.log(`  result: order ${run(failing)}`);
}

In TypeScript: Kind is a union of the three kinds, so a misspelt kind in the saga table is a compile error; the rest is the JavaScript program.

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import (
	"fmt"
	"slices"
)

type step struct {
	name         string
	kind         string // compensable, pivot or retryable
	compensation string
	why          string // why a run fails at this step
}

var saga = []step{
	{"create order", "compensable", "cancel order", ""},
	{"reserve stock", "compensable", "release stock", "out of stock"},
	{"authorise payment", "compensable", "void the authorisation", "card declined"},
	{"capture payment", "pivot", "", "capture refused"},
	{"book courier", "retryable", "", "courier unavailable"},
	{"send confirmation", "retryable", "", ""},
}

func run(failing string) string {
	var done []string
	for _, s := range saga {
		if s.name == failing && s.kind != "retryable" {
			fmt.Printf("  failed  %s: %s\n", s.name, s.why)
			for _, undo := range slices.Backward(done) { // compensations run newest first
				fmt.Printf("  undo    %s\n", undo)
			}
			return "CANCELLED"
		}
		if s.name == failing { // after the pivot: retried until it works
			for attempt := 1; attempt <= 2; attempt++ {
				fmt.Printf("  retry   %s: attempt %d failed (%s)\n", s.name, attempt, s.why)
			}
		}
		note := ""
		if s.kind == "pivot" {
			note = " (pivot: no way back)"
		}
		fmt.Printf("  done    %s%s\n", s.name, note)
		if s.compensation != "" {
			done = append(done, s.compensation)
		}
	}
	return "CONFIRMED"
}

func main() {
	cases := []string{""}
	for _, s := range saga {
		if s.why != "" {
			cases = append(cases, s.name)
		}
	}
	fmt.Println("One run per failing step")
	for _, failing := range cases {
		if failing == "" {
			fmt.Println("\nNo step fails")
		} else {
			fmt.Printf("\n'%s' fails\n", failing)
		}
		fmt.Printf("  result: order %s\n", run(failing))
	}
}

In Go: slices.Backward walks done from its newest compensation to its oldest, and the runs to make are collected in one loop over the saga.

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

enum Kind { case compensable, pivot, retryable }

// (step, kind, compensation, why a run fails at this step)
let saga: [(step: String, kind: Kind, compensation: String, why: String)] = [
  ("create order", .compensable, "cancel order", ""),
  ("reserve stock", .compensable, "release stock", "out of stock"),
  ("authorise payment", .compensable, "void the authorisation", "card declined"),
  ("capture payment", .pivot, "", "capture refused"),
  ("book courier", .retryable, "", "courier unavailable"),
  ("send confirmation", .retryable, "", ""),
]

func run(_ failing: String) -> String {
  var done: [String] = []
  for (step, kind, compensation, why) in saga {
    if step == failing && kind != .retryable {
      print("  failed  \(step): \(why)")
      // compensations run newest first
      for undo in done.reversed() {
        print("  undo    \(undo)")
      }
      return "CANCELLED"
    }
    if step == failing {
      // after the pivot: retried until it works
      for attempt in 1...2 {
        print("  retry   \(step): attempt \(attempt) failed (\(why))")
      }
    }
    print("  done    \(step)" + (kind == .pivot ? " (pivot: no way back)" : ""))
    if !compensation.isEmpty {
      done.append(compensation)
    }
  }
  return "CONFIRMED"
}

print("One run per failing step")
for failing in [""] + saga.filter { !$0.why.isEmpty }.map(\.step) {
  print()
  print(failing.isEmpty ? "No step fails" : "'\(failing)' fails")
  print("  result: order \(run(failing))")
}

In Swift: The tuple labels name each step's four fields, and done.reversed() gives the compensations newest first without changing done.

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

# why: why a run fails at this step
Step = Data.define(:name, :kind, :compensation, :why)

SAGA = [
  Step['create order', :compensable, 'cancel order', ''],
  Step['reserve stock', :compensable, 'release stock', 'out of stock'],
  Step['authorise payment', :compensable, 'void the authorisation', 'card declined'],
  Step['capture payment', :pivot, '', 'capture refused'],
  Step['book courier', :retryable, '', 'courier unavailable'],
  Step['send confirmation', :retryable, '', '']
].freeze

def compensate(step, done)
  puts "  failed  #{step.name}: #{step.why}"
  # compensations run newest first
  done.reverse_each { |undo| puts "  undo    #{undo}" }
  'CANCELLED'
end

def retry_twice(step)
  # after the pivot: retried until it works
  [1, 2].each do |attempt|
    puts "  retry   #{step.name}: attempt #{attempt} failed (#{step.why})"
  end
end

def run(failing)
  done = []
  SAGA.each do |step|
    return compensate(step, done) if step.name == failing && step.kind != :retryable

    retry_twice(step) if step.name == failing
    puts "  done    #{step.name}#{' (pivot: no way back)' if step.kind == :pivot}"
    done << step.compensation unless step.compensation.empty?
  end
  'CONFIRMED'
end

puts 'One run per failing step'
['', *SAGA.reject { |step| step.why.empty? }.map(&:name)].each do |failing|
  puts
  puts failing.empty? ? 'No step fails' : "'#{failing}' fails"
  puts "  result: order #{run(failing)}"
end

In Ruby: Data.define makes Step an immutable value, and RuboCop's limit of 10 lines a method splits run into compensate and retry_twice.

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

enum class Kind { COMPENSABLE, PIVOT, RETRYABLE }

// why: why a run fails at this step
data class Step(
    val name: String,
    val kind: Kind,
    val compensation: String,
    val why: String,
)

val saga =
    listOf(
        Step("create order", Kind.COMPENSABLE, "cancel order", ""),
        Step("reserve stock", Kind.COMPENSABLE, "release stock", "out of stock"),
        Step("authorise payment", Kind.COMPENSABLE, "void the authorisation", "card declined"),
        Step("capture payment", Kind.PIVOT, "", "capture refused"),
        Step("book courier", Kind.RETRYABLE, "", "courier unavailable"),
        Step("send confirmation", Kind.RETRYABLE, "", ""),
    )

fun run(failing: String): String {
    val done = mutableListOf<String>()
    for ((name, kind, compensation, why) in saga) {
        if (name == failing && kind != Kind.RETRYABLE) {
            println("  failed  $name: $why")
            // compensations run newest first
            for (undo in done.asReversed()) {
                println("  undo    $undo")
            }
            return "CANCELLED"
        }
        if (name == failing) {
            // after the pivot: retried until it works
            for (attempt in 1..2) {
                println("  retry   $name: attempt $attempt failed ($why)")
            }
        }
        println("  done    $name" + if (kind == Kind.PIVOT) " (pivot: no way back)" else "")
        if (compensation.isNotEmpty()) {
            done.add(compensation)
        }
    }
    return "CONFIRMED"
}

fun main() {
    println("One run per failing step")
    for (failing in listOf("") + saga.filter { it.why.isNotEmpty() }.map { it.name }) {
        println()
        println(if (failing.isEmpty()) "No step fails" else "'$failing' fails")
        println("  result: order ${run(failing)}")
    }
}

In Kotlin: The data class destructures in the for loop as (name, kind, compensation, why), and asReversed() is a reversed view of done, not a copy.

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

enum Kind { compensable, pivot, retryable }

// (step, kind, compensation, why a run fails at this step)
const saga = [
  ('create order', Kind.compensable, 'cancel order', ''),
  ('reserve stock', Kind.compensable, 'release stock', 'out of stock'),
  (
    'authorise payment',
    Kind.compensable,
    'void the authorisation',
    'card declined',
  ),
  ('capture payment', Kind.pivot, '', 'capture refused'),
  ('book courier', Kind.retryable, '', 'courier unavailable'),
  ('send confirmation', Kind.retryable, '', ''),
];

String run(String failing) {
  final done = <String>[];
  for (final (step, kind, compensation, why) in saga) {
    if (step == failing && kind != Kind.retryable) {
      print('  failed  $step: $why');
      // compensations run newest first
      for (final undo in done.reversed) {
        print('  undo    $undo');
      }
      return 'CANCELLED';
    }
    if (step == failing) {
      // after the pivot: retried until it works
      for (final attempt in [1, 2]) {
        print('  retry   $step: attempt $attempt failed ($why)');
      }
    }
    print(
      '  done    $step${kind == Kind.pivot ? ' (pivot: no way back)' : ''}',
    );
    if (compensation.isNotEmpty) {
      done.add(compensation);
    }
  }
  return 'CONFIRMED';
}

void main() {
  print('One run per failing step');
  final failing = saga.where((s) => s.$4.isNotEmpty).map((s) => s.$1);
  for (final name in ['', ...failing]) {
    print('');
    print(name.isEmpty ? 'No step fails' : "'$name' fails");
    print('  result: order ${run(name)}');
  }
}

In Dart: Each step is a record, destructured in the for-in loop, and $1 and $4 read a record's first and fourth fields.

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

One run per failing step

No step fails
  done    create order
  done    reserve stock
  done    authorise payment
  done    capture payment (pivot: no way back)
  done    book courier
  done    send confirmation
  result: order CONFIRMED

'reserve stock' fails
  done    create order
  failed  reserve stock: out of stock
  undo    cancel order
  result: order CANCELLED

'authorise payment' fails
  done    create order
  done    reserve stock
  failed  authorise payment: card declined
  undo    release stock
  undo    cancel order
  result: order CANCELLED

'capture payment' fails
  done    create order
  done    reserve stock
  done    authorise payment
  failed  capture payment: capture refused
  undo    void the authorisation
  undo    release stock
  undo    cancel order
  result: order CANCELLED

'book courier' fails
  done    create order
  done    reserve stock
  done    authorise payment
  done    capture payment (pivot: no way back)
  retry   book courier: attempt 1 failed (courier unavailable)
  retry   book courier: attempt 2 failed (courier unavailable)
  done    book courier
  done    send confirmation
  result: order CONFIRMED

Every failure up to the pivot ends with the order cancelled, after the compensations of the steps that ran, newest first; a refused capture voids the hold as well, and nothing is ever charged. The failure after the pivot ends with two retries and a confirmed order. No run ends half-done, which is the guarantee a saga gives in place of atomicity.

Key idea

Undo cheaply before the pivot and only move forward after it: the steps most likely to fail go first, the irreversible step after every check, and only retryable steps after that.

Checkpoint

Check: Where the pivot goes

  1. Question 1 of 2 Which step should be the pivot of an order saga that reserves stock, authorises the card and books a courier?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Capturing the payment, once the stock is reserved and the card authorised

    The pivot is the step after which the saga cannot turn back, so it should come after every step likely to reject the order. Capturing the payment is that step; the courier booking after it is retried until it succeeds.

  2. Question 2 of 2 A team adds "add loyalty points", with "remove loyalty points" as its compensation, after "capture payment". What is wrong with the design?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A failure after the pivot is retried, never undone, so that compensation can never run

    After the pivot the saga is committed to finishing, so its later steps are retried until they succeed. A compensable step placed there has an undo that the saga will never call; make it retryable, or move it before the pivot. The exercise below checks a saga for exactly this.

Orchestration keeps the flow in one place; choreography spreads it

Orchestration: an orchestrator commands stock, payment and shipping services. Choreography: each service reacts to the previous event.Orchestration:one coordinator sendscommands and waitsfor repliesChoreography:each service reactsto the event before itOrder orchestrator(keeps the saga's state)Each serviceanswers theorchestratorOrder serviceStock servicePayment serviceShipping serviceStock service1. reserve2. reservedPayment service3. charge4. chargedShipping service5. book couriercommandsand repliesOrderPlacedStockReservedPaymentCapturedPaymentFailed: release

The same saga, orchestrated and choreographed

Text description of the diagram

The diagram has two panels, one above the other.

  1. Orchestration: an order orchestrator, which keeps the saga's state, exchanges commands and replies with three services listed below it, numbered in order: it sends "reserve" to the stock service and gets "reserved" back, sends "charge" to the payment service and gets "charged" back, then sends "book courier" to the shipping service.
  2. Choreography: there is no coordinator. The order service publishes OrderPlaced, which the stock service reacts to; the stock service publishes StockReserved, which the payment service reacts to; the payment service publishes PaymentCaptured, which the shipping service reacts to. A dashed arrow shows the failure path: on PaymentFailed, the stock service releases the reservation.

In orchestration, a coordinator keeps the saga’s state, sends each service a command and decides the next step from the reply. The whole flow lives in one place, easy to read, change and monitor, at the cost of one more component that must stay available.

In choreography, there is no coordinator. Each service listens for the events of the step before and publishes its own, so the services stay loosely coupled, but the flow exists only as the sum of their subscriptions, which makes it hard to see, test and change. Step through the same declined card in both styles:

Interactive · message by message The same declined card, orchestrated and choreographed Step through the first steps of the order saga twice. In the first run the order service commands each step; in the second each service reacts to the event before it.
The same declined card, orchestrated and choreographed, step 8 of 8. Messages so far: Order service to Stock service: reserve 1 unit; Stock service to Order service: reserved; Order service to Payment service: authorise card; Order service to Stock service: release stock; Order service to Stock service: OrderPlaced; Stock service to Payment service: StockReserved; Payment service to Stock service: PaymentFailed; Stock service to Order service: StockReleased.Order servicepublishes OrderPlacedStock servicePayment servicereserve 1 unitreservedauthorise cardrelease stockOrderPlacedStockReservedPaymentFailedStockReleased

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Orchestration: reserve. The orchestrator in the order service saves the saga's state and commands the stock service to reserve one unit.
  2. The reply. Stock commits the reservation and replies. The orchestrator records the reply before it decides the next step.
  3. Authorise. The orchestrator commands the payment service to authorise the card, and the card is declined.
  4. The orchestrator compensates. On the declined reply the orchestrator itself sends the compensation. The whole flow, success and failure, is in its code, and one row of its state says where the saga is.
  5. Choreography: an event. Run it again without a coordinator. The order service commits the order and publishes OrderPlaced; it commands nobody.
  6. Stock reacts. Stock subscribes to OrderPlaced, reserves a unit and publishes StockReserved, which the payment service subscribes to.
  7. Payments reacts. The card is declined, so payments publishes PaymentFailed. It does not know who will undo what; it only announces what happened.
  8. Each one undoes its own step. Stock releases the unit when it sees PaymentFailed, and the order service cancels the order on the event that follows. The flow exists only as the sum of the services' subscriptions.

Azure weighs the two the same way: choreography for simple flows with few services, orchestration for complex flows that change. Answer four questions about your saga, and the table marks the style that fits and why:

Interactive · answer and compare Orchestrate or choreograph your saga? Answer four questions about the saga you are designing. The table marks the style that fits and says why.
How many steps does the saga have?
How often does its flow change, and how many teams change it?
Does it move money, or must someone be able to say where each saga stands?
Can one team own and run a coordinator for it?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Orchestration and choreography against what decides between them
Style Seeing where a saga is Adding a step Coupling Moving parts
Orchestration Who picks the next step: one coordinator, from the saga state it saves Good one row of the orchestrator's state Good change the orchestrator, add the service Mixed services know commands, not each other Mixed one more component, which must stay available
Choreography Who picks the next step: every service, from the events it subscribes to Poor join the logs of every service Poor change what two or more services publish and consume Good services know only each other's events Mixed none extra, but event chains can form cycles
How the answers decide
  • “How many steps does the saga have?” Four or more: adds a caveat to Choreography. With four or more steps the flow exists only as the sum of many subscriptions, which is hard to see, test and change.
  • “How often does its flow change, and how many teams change it?” Often, or several teams change it: rules out Choreography. Each change touches what several services publish and consume, and their releases have to be ordered so no event is lost in between.
  • “Does it move money, or must someone be able to say where each saga stands?” Yes: adds a caveat to Choreography. Finding a stuck payment means joining every service's logs; an orchestrator answers with one row of its state.
  • “Can one team own and run a coordinator for it?” No, nobody would own it: adds a caveat to Orchestration. An orchestrator is one more component that must stay available; without an owner it becomes the part nobody fixes.
  • “How many steps does the saga have?” Two or three and “How often does its flow change, and how many teams change it?” Rarely, and one team owns it: adds a caveat to Orchestration. For a short, stable chain an orchestrator adds a moving part that buys little; Azure suggests choreography for such simple flows.

Checkpoint

Check: Orchestration or choreography

  1. Question 1 of 2 In which style can one row of state say where a stuck saga is?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Orchestration, because the orchestrator keeps each saga's state

    Orchestration keeps the flow and the state in one place, which makes a stuck saga one query away. In choreography the flow exists only as the sum of the services' subscriptions.

  2. Question 2 of 2 A two-step flow, "order paid, then send the receipt", is owned by one team and has not changed in a year. Which style fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Choreography, with the receipt service reacting to the paid event

    Azure's guidance matches the lesson's table: choreography for simple flows with few services, orchestration for complex flows that change. Two stable steps are the simple case.

Practise: plan the recovery

Write the two rules the stepper follows: what a failure at a given step must undo, and which compensable steps sit where their compensation could never run:

Exercise · Medium · Python

Plan a saga's recovery and check where its pivot sits

A saga is a list of steps. Each step is a dictionary with a "name", a "kind" ("compensable", "pivot" or "retryable") and an "undo": the name of its compensation, or None for the pivot and retryable steps. Write two functions in recovery.py.

recovery_plan(saga, failed) says what to do when the step at index failed fails. That step's own local transaction has already rolled back, so it needs no compensation.

  • If the failed step is compensable or the pivot, return {"action": "compensate", "undo": [...]}: the compensations of every step before it, newest first.
  • If the failed step is retryable, the pivot has already succeeded and there is no way back, so return {"action": "retry", "undo": []}.

misplaced(saga) returns the names of compensable steps that come after the pivot, in saga order. Such a step can never be compensated, because a failure after the pivot is retried, not undone. A saga without a pivot has no misplaced steps.

For a saga of create order, reserve stock (both compensable), capture payment (the pivot) and book courier (retryable), a failure of capture payment returns {"action": "compensate", "undo": ["release stock", "cancel order"]}.

Starter code · recovery.py

def recovery_plan(saga, failed):
    """What to do when step number `failed` of the saga fails."""
    # Replace this line with your code.
    return {"action": "retry", "undo": []}


def misplaced(saga):
    """Names of compensable steps placed after the pivot."""
    # Replace this line with your code.
    return []
The sample tests · test_recovery.py
from recovery import misplaced, recovery_plan

ORDER = [
    {"name": "create order", "kind": "compensable", "undo": "cancel order"},
    {"name": "reserve stock", "kind": "compensable", "undo": "release stock"},
    {"name": "authorise payment", "kind": "compensable", "undo": "void authorisation"},
    {"name": "capture payment", "kind": "pivot", "undo": None},
    {"name": "book courier", "kind": "retryable", "undo": None},
]


def test_first_step_fails():
    """a failure of the first step needs no compensation"""
    assert recovery_plan(ORDER, 0) == {"action": "compensate", "undo": []}


def test_pivot_fails():
    """a failed pivot undoes every earlier step, newest first"""
    assert recovery_plan(ORDER, 3) == {"action": "compensate", "undo": ["void authorisation", "release stock", "cancel order"]}


def test_middle_step_fails():
    """a failed compensable step undoes only the steps before it"""
    assert recovery_plan(ORDER, 2) == {"action": "compensate", "undo": ["release stock", "cancel order"]}


def test_after_the_pivot():
    """a step after the pivot is retried, never compensated"""
    assert recovery_plan(ORDER, 4) == {"action": "retry", "undo": []}


def test_misplaced_steps():
    """compensable steps after the pivot are reported in saga order"""
    bad = ORDER[:4] + [
        {"name": "add loyalty points", "kind": "compensable", "undo": "remove loyalty points"},
        {"name": "book courier", "kind": "retryable", "undo": None},
        {"name": "send voucher", "kind": "compensable", "undo": "revoke voucher"},
    ]
    assert misplaced(bad) == ["add loyalty points", "send voucher"]
    assert misplaced(ORDER) == []
    assert misplaced(ORDER[:3]) == []
A hint

For recovery_plan, look at saga[failed]["kind"] first: a retryable step decides the answer on its own. Otherwise collect step["undo"] for every step in saga[:failed] and reverse the list. For misplaced, find the index of the pivot, if there is one, and keep the compensable steps after it.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then practise the three ideas above. The session leads with whichever one your answers have shown to be the weakest.

Practice that adapts to you

Practise this lesson

9 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 What does a saga give up, compared with two-phase commit?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: Isolation, because other requests see each step as soon as it commits

    A saga trades isolation for availability: nothing holds locks across services, so nothing blocks, and other requests may see and act on its intermediate state.

  2. Question 2 of 9 In Garcia-Molina and Salem's airline example, a compensation cancels a seat by subtracting one from the reservations count. Why not write back the count read before the booking?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: Other bookings may have changed the count in the meantime, and writing back the old value would erase them

    Compensations are business actions, not restores. Undoing one booking means subtracting one, which stays right however many other bookings committed between the step and its compensation.

  3. Question 3 of 9 A saga sent a confirmation email, and then a later step failed. What can the email step's compensation do?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Send a correction, because a sent email stays sent

    Compensation is not rollback. A step whose effect cannot be withdrawn gets a compensation that corrects it, which is also a reason to send emails after the pivot rather than before.

  4. Question 4 of 9 What makes a saga step retryable?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: It comes after the pivot and is idempotent, so it is repeated until it succeeds

    Azure sorts saga steps into compensable, pivot and retryable. Retryable steps follow the pivot and are idempotent, so repeating them after a failure is safe.

  5. Question 5 of 9 In the stepper's run where capture payment fails, which compensations run, and in which order?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: Void the authorisation, release stock, cancel order

    A failed pivot is still a failure before the point of no return. The stepper prints the three undos newest first and ends with the order cancelled.

  6. Question 6 of 9 A shop's saga charges the card first and reserves stock second. What does every out-of-stock order cost it?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: A refund, which the customer sees as money leaving and coming back

    Put the steps most likely to fail before the pivot, where they cost a cheap compensation, and the irreversible step after every check that might reject the order.

  7. Question 7 of 9 What does an orchestrator do in a saga?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: It keeps the saga's state, sends each service a command and decides the next step from the reply

    In orchestration one coordinator owns the flow; in choreography each service reacts to the event of the step before and publishes its own.

  8. Question 8 of 9 Some orders of a choreographed flow stay pending for days. Why is it hard to say where each one is stuck?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: The flow exists only as the sum of the services' subscriptions, so you have to join every service's logs

    Choreography keeps services loosely coupled, at the cost of a flow nobody sees whole. Orchestration answers the same question with one row of state.

  9. Question 9 of 9 A payment flow of six steps is changed every month by three teams, and support must be able to say where any order is. Which style fits?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: Orchestration, so the flow and its state live in one place that one team owns

    Four or more steps, frequent change or money involved point to orchestration; two or three steps whose owners rarely change them point to choreography.

Failure drills: crashes, unknown outcomes and sagas that collide Pro: opens with a Pro, Premium or Ultimate pass

A saga’s happy path is easy to write; its failures sit between the steps. This part crashes an orchestrator in the middle of a saga, makes a compensation fail for good, leaves a payment’s outcome unknown, and runs 150 buyers into the gap that a saga leaves open.

In this part:

  • An orchestrator that crashes and resumes, reproduced
  • A payment step whose outcome is unknown
  • Sagas that see each other’s half-finished work, measured

2 diagrams · 2 runnable examples (JavaScript, Python) · about 12 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Read a complete sample first: Design a unique ID generator

Choosing a design with numbers, and how answers are judged Pro: opens with a Pro, Premium or Ultimate pass

The pivot’s position and the coordination style are both decisions with costs you can count. This part puts numbers on them for 100,000 orders and ends with what a good interview answer covers at each level.

In this part:

  • Where the pivot goes, in refunds per 100,000 orders
  • Messages, latency and change, counted for both styles
  • Finding stuck sagas before customers do
  • What a good answer covers at each level

1 runnable example (Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    Why is a compensating transaction not the same as a rollback?

    A rollback undoes a transaction that never committed: the database discards its changes, and nobody else ever saw them. A saga's steps have already committed in their own services, other requests may have read or built on their results, and some effects cannot be withdrawn at all: a sent email, a shipped parcel, money that has left an account. A compensating transaction is therefore a new action with the opposite business effect, such as releasing a reservation, voiding a card hold or issuing a refund. It does not have to restore the data exactly; it has to leave the business in an acceptable state, and it must be safe to run twice, because it can be retried after a crash.

  2. Mid to senior

    A trip booking reserves a flight seat, a hotel room and an airport cab from three providers, then charges the card. Design the saga: the steps, their compensations, where the pivot goes, and what the customer sees while it runs.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Senior to staff

    Your order flow is choreographed across six services. Some orders stay "pending" for days and nobody can say why. Would you move to orchestration? Say what you would change, what it costs and how you would migrate.

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    In an order saga, the call that captures the payment times out. The money may or may not have moved. What should the saga do next, and what must it never do?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A saga is a sequence of local transactions, each with a compensation that undoes it in business terms: either every step runs, or the completed steps are compensated in reverse order.
  • A compensation is a new action, not a rollback, and other requests may already have seen the step it undoes.
  • Put the steps most likely to fail before the pivot, the irreversible step after every check, and only retryable steps after it.
  • Orchestration keeps the flow in one place; choreography keeps services decoupled, at the cost of a flow that nobody sees whole.

Terms in this lesson

choreography (saga)
Running a saga without a coordinator: each service reacts to the events of the step before and publishes its own, so the flow is the sum of the services' subscriptions.
compensating transaction
A new transaction with the opposite business effect of a step that has already committed, such as releasing a reservation or issuing a refund. It does not restore old values; it leaves the business in an acceptable state.
orchestration (saga)
Running a saga from one coordinator that keeps the saga's state, sends each service a command and decides the next step from the reply.
pivot (saga)
The step of a saga after which it can no longer turn back, such as capturing a payment. The steps before it can be compensated; the steps after it are retried until they succeed.
saga
A business transaction across several services, run as a sequence of local transactions, each paired with a compensating transaction that undoes its effect if a later step fails.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.