Introduction

Every booking you've ever made online is secretly a chain of promises. Reserve a flight. Charge a card. Notify the airline. Update loyalty points. If any link breaks, all the earlier links must unwind — cleanly, without leaving you charged for a seat you don't have.

The classical answer is an ACID transaction: wrap everything in one big lock, commit once, rollback on failure. That works perfectly inside a single database. But once your workflow crosses service boundaries — an inventory service, a payment service, a notification service — holding one lock for seconds or minutes becomes a bottleneck, a deadlock risk, and often simply impossible.

Hector Garcia-Molina and Kenneth Salem described the solution in their 1987 paper: a saga. Break the long-running operation into a sequence of smaller local transactions, each of which commits immediately. If a later step fails, run compensating transactions in reverse order — each one undoing the effect of the step before it. No global lock ever held; eventual consistency guaranteed.

The pattern is so fundamental that every major cloud architecture guide lists it. Understanding it means understanding how the systems behind flights, hotel bookings, and e-commerce orders actually stay sane.

Try It: Roll Back a Saga

Below is a four-step booking saga: reserve a seat, charge the card, notify the airline, and award loyalty points. Each step commits locally. Press Run saga to advance through the steps one by one. At any point, press Inject failure to simulate a step crashing — and watch the saga automatically execute the compensating transactions in reverse to undo all earlier work.

<!-- Saga pattern demo — root element -->
<div class="saga-app">
  <p class="hint">Each row is one saga step. Press <b>Run saga</b> to commit steps one by one. Press <b>Inject failure</b> to simulate a crash — the saga runs its compensating transactions in reverse to undo all committed work.</p>
  <div id="steps-list" class="steps-list"></div>
  <div class="status-bar" id="status-bar"></div>
  <div class="btns">
    <button id="btn-run" type="button">Run saga</button>
    <button id="btn-fail" type="button" class="danger">Inject failure</button>
    <button id="btn-reset" type="button" class="ghost">Reset</button>
  </div>
</div>
/* Styles for the saga step list and state indicators */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.saga-app { padding: .4rem 0; }
.hint { font-size: .87rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.steps-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: .8rem; }
.step { display: flex; align-items: center; gap: 10px; padding: .5rem .75rem;
        border-radius: 8px; border: 1.5px solid #cdd9e3; background: #f4f7fa;
        font-size: .9rem; transition: all .25s; }
.step .icon { width: 22px; text-align: center; font-size: 1.1rem; flex-shrink: 0; }
.step .label { flex: 1; font-weight: 600; }
.step .comp { font-size: .78rem; color: #888; margin-left: auto; white-space: nowrap; }
/* pending state — not yet reached */
.step.pending  { background: #f4f7fa; border-color: #cdd9e3; }
/* running state — currently executing */
.step.running  { background: #fff8e1; border-color: #f0c040; }
/* done state — committed successfully */
.step.done     { background: #e8f5e9; border-color: #66bb6a; }
/* failed state — step crashed */
.step.failed   { background: #fce8e8; border-color: #e57373; }
/* compensating state — undoing this step */
.step.compensating { background: #fff3e0; border-color: #ffa726; }
/* compensated state — undo complete */
.step.compensated  { background: #f3e5f5; border-color: #ab47bc; }
.status-bar { font-size: .95rem; font-weight: 600; min-height: 1.5em; margin-bottom: .5rem; }
.status-bar.ok  { color: #2e7d32; }
.status-bar.bad { color: #c62828; }
.status-bar.warn { color: #e65100; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border-radius: 8px;
         cursor: pointer; border: 1.5px solid #1d3557; background: #1d3557; color: #fff; }
button.danger { background: #c62828; border-color: #c62828; }
button.ghost  { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Saga pattern: a sequence of local transactions with compensating transactions.
// Each step has a forward action (label) and a compensating action (comp).
const STEPS = [
  { id: 'seat',    icon: '✈️', label: 'Reserve seat',    comp: 'Release seat' },
  { id: 'payment', icon: '💳', label: 'Charge card', comp: 'Issue refund' },
  { id: 'airline', icon: '📨', label: 'Notify airline', comp: 'Cancel notification' },
  { id: 'loyalty', icon: '⭐', label: 'Award loyalty points', comp: 'Revoke points' }
];

// Application state
let stepStates  = STEPS.map(() => 'pending');
let cursor      = 0;   // index of the next step to execute
let failed      = false;
let compensating = false;
let done        = false;

function setState(idx, st) { stepStates[idx] = st; renderSteps(); }

function renderSteps() {
  const list = document.getElementById('steps-list');
  list.innerHTML = '';
  STEPS.forEach((s, i) => {
    const div = document.createElement('div');
    div.className = 'step ' + stepStates[i];
    div.innerHTML =
      '<span class="icon">' + s.icon + '</span>' +
      '<span class="label">' + s.label + '</span>' +
      '<span class="comp">' + (
        stepStates[i] === 'compensating' || stepStates[i] === 'compensated'
          ? '↩ ' + s.comp
          : ''
      ) + '</span>';
    list.appendChild(div);
  });
  const runBtn  = document.getElementById('btn-run');
  const failBtn = document.getElementById('btn-fail');
  runBtn.disabled  = done || compensating;
  failBtn.disabled = done || cursor === 0 || compensating || failed;
}

function setStatus(msg, cls) {
  const el = document.getElementById('status-bar');
  el.textContent = msg;
  el.className   = 'status-bar ' + (cls || '');
}

// Advance the saga by one step (commit forward).
async function advance() {
  if (done || compensating) return;
  if (cursor < STEPS.length) {
    setState(cursor, 'running');
    setStatus(fmt('Executing: %label%…', { label: STEPS[cursor].label }), 'warn');
    await pause(500);
    setState(cursor, 'done');
    cursor++;
    if (cursor === STEPS.length) {
      done = true;
      setStatus('✓ Saga complete — all steps committed successfully.', 'ok');
      document.getElementById('btn-run').disabled = true;
      document.getElementById('btn-fail').disabled = true;
    } else {
      setStatus(fmt('✓ Committed: %label%. Ready for next step.', { label: STEPS[cursor - 1].label }), 'ok');
    }
  }
}

// Inject a failure at the current step and run compensations backward.
async function injectFail() {
  if (done || compensating || cursor === 0 || failed) return;
  failed = true;
  compensating = true;
  setState(cursor, 'failed');
  setStatus(fmt('✗ Step failed: %label%. Running compensations in reverse…', { label: STEPS[cursor].label }), 'bad');
  await pause(600);
  // Walk backward through committed steps and compensate each one.
  for (let i = cursor - 1; i >= 0; i--) {
    setState(i, 'compensating');
    setStatus(fmt('Undoing %label% (compensation: %comp%)…', { label: STEPS[i].label, comp: STEPS[i].comp }), 'warn');
    await pause(700);
    setState(i, 'compensated');
  }
  compensating = false;
  done = true;
  setStatus('✗ Saga rolled back — all compensations applied.', 'bad');
}

function pause(ms) { return new Promise(r => setTimeout(r, ms)); }

function reset() {
  stepStates   = STEPS.map(() => 'pending');
  cursor       = 0;
  failed       = false;
  compensating = false;
  done         = false;
  renderSteps();
  setStatus('', '');
}

document.getElementById('btn-run').addEventListener('click', advance);
document.getElementById('btn-fail').addEventListener('click', injectFail);
document.getElementById('btn-reset').addEventListener('click', reset);

renderSteps();
setStatus('Press Run saga to start.', '');
Result
Console

Notice that compensation is not rollback. A compensating transaction is a new forward action that logically reverses the effect of an earlier one — for example, issuing a refund rather than reversing a database write. Each step must be designed to be compensable from the start.

The Real Complexity

Sagas look simple: commit forward, compensate backward. The real difficulty lives in the corners.

  • Compensation can fail. A saga step that charged a card can in principle be compensated by a refund — but what if the refund API is also down? Real implementations need retry logic and idempotency keys so compensations can be safely re-attempted without double-refunding.
  • Order matters. Steps that can be compensated are called compensable; steps that cannot (because their effect is already visible to the outside world, like sending an email) are called pivot steps. A correct saga places all compensable steps before the pivot step. Once you cross the pivot, there is no clean undo.
  • Isolation is gone. Classical ACID gives you isolation: other transactions don't see your intermediate state. A saga has no such guarantee — another saga can read a half-committed state. This opens the door to anomalies like the lost update or the dirty read. Saga designers must reason about which anomalies their business logic can tolerate.
  • Two styles. A choreography saga has each service emit events that trigger the next service — simple to build, hard to trace when something goes wrong. An orchestration saga uses a central coordinator (a saga orchestrator) that issues commands and records the saga's state — easier to monitor and debug, at the cost of a new central component.

The pattern was described as solved by Garcia-Molina and Salem in 1987, but the engineering trade-offs — idempotency, pivot placement, anomaly tolerance — are decided fresh for every workflow. Unlike a simple scheduling problem where optimal is well-defined, a correct saga design depends on what your business can actually tolerate.

Where It Matters

Any workflow that must stay consistent while touching more than one service lives in saga territory:

  • E-commerce checkout: reserve stock, charge payment, create shipment, send confirmation — four services, four local commits, and a compensation chain ready to fire if any step fails.
  • Airline booking: hold a seat, process payment, issue a ticket, update frequent-flyer miles — each managed by a separate legacy system that cannot share a database transaction.
  • Bank transfers: debit the source account, credit the destination, notify both customers — the compensation for a failed credit is a reversal debit on the source.
  • Healthcare scheduling: book an appointment slot, verify insurance, notify the provider, send a patient reminder — a failed insurance check must release the slot and suppress the notifications.
  • Microservice orchestration frameworks: AWS Step Functions, Netflix Conductor, Temporal, and Axon Framework all implement saga-style orchestration under the hood.

Wherever you see a multi-step workflow with "undo" requirements, you are looking at a saga. The pattern is the distributed-systems answer to the same question that load balancing asks at the infrastructure level: how do you keep a system consistent and available when no single node controls everything?

Conclusion

The saga pattern is a reminder that consistency is negotiable. Classical transactions give you all-or-nothing atomicity, but they demand a shared lock — a luxury distributed systems cannot always afford. Sagas trade that luxury for availability, breaking the workflow into steps that each commit fast and each know how to undo themselves.

That trade is never free. Compensations must be designed, idempotency must be enforced, and pivot steps must be placed carefully. But the reward is a system that stays available under partial failures — and that, at scale, is usually worth every extra line of compensation logic.

The next time you get a refund e-mail after a booking fails, you've seen a saga run its compensation chain to completion. Behind the scenes, each service ran its undo step in reverse order, leaving the world exactly as it was before you clicked "Book." Not magic — just a 1987 paper made software.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/saga-pattern/Content licensed under CC BY-NC 4.0.