Designing Data-Intensive Applications
Case 8

Sagas and Orchestration

Long-running flows span services. Sagas coordinate forward steps and compensating rollbacks.

A multistep checkout is not one database transaction — payment, inventory, shipping, and email span systems. Sagas make partial failure explicit instead of hoping for the best.

typescript — Saga step with compensation
// Hold → pay → confirm with compensation
async function bookingSaga(bookingId: string) {
  const hold = await inventory.hold(bookingId);
  try {
    const charge = await stripe.charge({ idempotencyKey: bookingId });
    await inventory.confirm(hold.id);
    await kafka.publish("booking.confirmed", { bookingId, chargeId: charge.id });
  } catch (err) {
    await inventory.release(hold.id); // compensating action
    throw err;
  }
}
Diagram

Step-by-step walkthrough

Forward saga — happy path

  • ① Start checkout — Client kicks off a durable workflow with a correlation ID.
  • ② Reserve stock — Orchestrator calls inventory service; hold ID returned and persisted in workflow history.
  • ③ Capture payment — Payments service charges card; idempotent on retry with same key.
  • ④ Create shipment — Shipping service creates label only after payment succeeds.
  • ⑤ Order complete — Client notified; workflow marked succeeded in orchestrator history.

Compensation — payment failure

  • FAILED — Stripe returns card_declined; orchestrator branches to compensate, not retry forever.
  • ⑥ Compensate: release hold — Inventory hold released before error returned to client — no orphaned locks.
Diagram
Diagram
In practice

Temporal, AWS Step Functions, and Cadence power durable workflows at Uber, Netflix, and Stripe. Kafka-only choreography works until you need a human-readable workflow graph.

Why these technologies?

Why Temporal / Step Functions / Cadence?

Durable workflow state survives worker crashes. Timers (wait 7 days for KYC) without holding threads. Replay history for debugging failed checkouts.

Why Saga pattern (not 2PC)?

Payment, inventory, and shipping are separate services — XA transactions across Stripe and your DB don't exist. Compensating actions (release hold) are explicit.

Why Kafka (choreography option)?

Services react to events without a central orchestrator — looser coupling, harder to visualize. Many teams start with orchestration, add events for notifications.

Why Per-service PostgreSQL?

Each bounded context owns its data — inventory service DB, payment service ledger. Shared monolith DB creates deployment coupling.

Why Idempotent activity implementations?

Retries are guaranteed — charge API must accept same idempotency key, ship API must tolerate duplicate create with same correlation ID.

Why Dead-letter queue?

Poison messages (bad payload) isolate without blocking the whole saga — ops can inspect and replay.

Key Takeaways
  • Choreography: each service emits events; others react (loose coupling).
  • Orchestration: central workflow engine calls services in order (clear visibility).
  • Each step idempotent; correlation ID ties the saga together.
  • Compensating transactions undo prior steps (cancel shipment, refund hold).
  • Timeouts trigger automatic compensation or human escalation.
  • Shopify checkout and Uber trip lifecycle are canonical sagas.
sagaorchestrationchoreographyTemporalcompensationKafka