Multi-step processes — saga, choreography, orchestration, idempotency

Multi-step processes & sagas for system design interviews

Reliable multi-step workflows end to end: single-server pitfalls, sagas and compensation, event-driven choreography vs orchestration, Temporal durable execution, Step Functions, signals, idempotency, versioning, and interview deep dives.

Why multi-step processes dominate oncall

Clean database tutorials deal with one write. Real apps coordinate dozens of flaky services to fulfill an order, dispatch a ride, or run an agent pipeline. Doing that quickly and reliably is a continual source of operational pain — and a favorite interview topic.

Jimmy Bogard's Six Little Lines of Fail captures it: distributed systems make even a simple sequence surprisingly hard. This post is the deep dive behind Common patterns. Pair with Managing long-running tasks (simple job chains), Message queues, Kafka, and Contention (single-DB vs cross-service).

01Problem

Crashes · webhooks · humans.

02Saga

Forward + compensate.

03Choreography

Event log · workers.

04Orchestration

Temporal · Step Fn.

Solution ladder from single server to workflow engine.
Walk the ladder — take the first rung that fits, graduate when pain demands it.

The problem

Consider e-commerce order fulfillment: charge payment, reserve inventory, create a shipping label, wait for a warehouse worker to pick the item, send confirmation email. Each step calls different services or waits on humans. Any can fail or timeout. Payment gateways callback via webhook minutes later. Your server might crash or deploy mid-flight. Business rules change the step order.

Happy path charge-reserve-label-pack-confirm with red dashed failure and wait annotations.
Happy path in black — refunds, gateway waits, machine downtime, and retries in red.

You can patch organically: fortify each service, delay queues for waits, webhooks for humans — but each patch adds complexity. You interweave system-level concerns (crashes, retries) with business-level concerns (item not found?). Not a great design.

Single-server primitives

The simplest build: one API server walks the steps in order, then responds.

async function fulfillOrder(order: Order): Promise<OrderResult> {
  try {
    await chargePayment(order);
    await reserveInventory(order);
    // Woops, server crashed here!
    await createShippingLabel(order);
    await sendConfirmationEmail(order);
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
}
Client to API Server hub-and-spoke to payment inventory shipping email with crash and webhook callouts.
Hub-and-spoke on one host — crash mid-flight or a webhook to the wrong box kills the flow.

Crashes: server dies after charge, before reserve — on restart, no memory of progress.

Callback routing: payment gateway webhooks hit a different host behind the load balancer — that host doesn't know which in-flight order the callback belongs to.

Persisted state & hand-rolled orchestration

Patch: persist order state after each step; route webhooks through pub/sub so any server can load the order and continue. That gets you something like:

API Server with Order State DB, Pub/Sub for payment webhooks, and downstream services.
State in a DB + pub/sub for webhooks — still hand-rolled pollers, locks, and compensation.
  • DB remembers progress but doesn't act on it — need a poller, locks, retry logic
  • Stalled orders sit until something notices
  • Compensation still manual: inventory fails → refund payment
  • Real implementation becomes tangled spaghetti

If you've operated one of these, you know the ongoing pain. That's why teams reach for workflow engines.

The saga pattern

Once steps accumulate with interesting failure handling, you're in saga territory — a long series of local steps, each with a matching compensating action. Forward on success; walk backward on failure.

Forward saga steps and backward compensations.
Ship fails → release inventory → refund charge.

Why not one big distributed transaction? Nobody holds a lock that long. 2PC stalls everyone if the coordinator stalls; payment gateways don't speak your transaction protocol. Sagas accept eventual consistency windows and guarantee undoability.

After charge, customer is out money before ship — order sits pending. Compensations need the same paranoia: refunds fail too → retries, idempotency, human escape hatch.

  • Choreography — no coordinator; workers react to events (independent teams, mid complexity)
  • Orchestration — one coordinator owns the flow (complex workflows, central visibility)

Event-driven choreography

Store the stream of events that got you here; workers react to drive forward. Durable log (Kafka popular; Redis Streams for lighter cases). Close cousin to event sourcing — here the log mainly coordinates work.

Client and API write to Event Store; payment inventory shipping email workers consume; payment webhook returns.
Event Store coordinates workers — flow emerges from reactions, not a central flowchart.

Payment worker sees OrderPlaced, calls gateway; webhook → PaymentCharged or PaymentFailed. Inventory worker sees PaymentCharged. Compensation backwards: InventoryFailed → payment worker emits PaymentRefunded.

  • Fault tolerance — consumer groups; partition reassignment resumes from committed offset (idempotent workers)
  • Scalability — add workers up to partition count
  • Observability — log is audit trail (but debugging "why no worker?" needs tooling)
  • Flexibility — append new reactions easily; inserting mid-chain changes event contracts

Workflow orchestration

For orchestration, describe a reliable long-running process that survives failures and continues where it left off — without hand-rolling infrastructure.

Central orchestrator calling activities with durable history.
Explicit flow in one place — Temporal or Step Functions.

Two camps: durable execution engines (code-first, Temporal) and managed workflow systems (declarative state machines, AWS Step Functions).

Durable execution engines (Temporal)

Write long-running code that moves between machines and survives crashes. Temporal (2019 fork of Uber's Cadence) is the popular open-source option.

const { processPayment, reserveInventory, shipOrder,
        sendConfirmationEmail, refundPayment } = proxyActivities(...);

async function myWorkflow(input: Order): Promise<OrderResult> {
  const paymentResult = await processPayment(input);
  if (paymentResult.success) {
    const inventoryResult = await reserveInventory(input);
    if (inventoryResult.success) {
      await shipOrder(input);
      await sendConfirmationEmail(input);
      return { success: true };
    } else {
      await refundPayment(input);
      return { success: false, error: "Inventory reservation failed" };
    }
  }
  return { success: false, error: "Payment failed" };
}

Looks like single-server orchestration — the difference is how it runs.

Workflow vs Activity

  • Workflow — deterministic recipe; given same inputs + history, same decisions. No network I/O here.
  • Activity — one step touching the outside world; must be idempotent (safe to retry).

Recovery by replay

Every Activity result recorded in history. Crash → new worker re-executes Workflow from top. Completed Activities return recorded results — no re-fire. Side-effect-free replay lands exactly where crash interrupted.

Crash after reserveInventory; replay skips completed activities.
Customer charged once · inventory reserved once · business code unaware of crash.
Client and API into Temporal Cluster with server and history database, workflow and activity workers, external services.
Temporal Cluster books tasks and history — workers run your code; activities call the outside world.

Signals wait for external events efficiently — human pickup, document signed — without holding a thread for 30 days.

Workflow waits on signal from webhook via engine API.
Persisted state + durable timer · worker freed while waiting.

Managed workflow systems

Declarative state machines in JSON/YAML — AWS Step Functions, Google Cloud Workflows. Less expressive than code; no clusters to run (serverless). Standard Step Functions: up to 1 year, 256KB between states.

In AWS shops you'd often generate definitions from CDK — the "ugly JSON" complaint is less important than understanding the state machine model and recovery semantics.

SystemStyleBest forWatch-out
TemporalCode (Workflow + Activity)Complex branching, long waits, full controlOperate cluster or Temporal Cloud
AWS Step FunctionsDeclarative JSONAWS-native, serverlessLess expressive; payload limits
Durable Functions (Azure)Code + triggersAzure shopsLess flexible than Temporal
AirflowPython DAGsScheduled batch / ETLNot event-driven user flows

When to use in interviews

Spot state machines and multi-step flows. Mid-level: name the need + one engine. Senior+: defend choice, walk compensation, field deep dives.

  • Payment / e-commerce — charge + inventory + ship; don't leave money captured without goods
  • Human-in-the-loop — Uber driver accept, loan approval, document signature
  • Agent pipelines — long tool chains with retries and HITL
  • Onboarding / provisioning — multi-day waits across services
Uber-style ride matching: Rider and Driver services through Temporal to Matching Workers with propose and notify loops.
Interview sketch: request → Temporal → match loop (propose / accept / timeout) → notify rider.

Common deep dives

What if the saga coordinator crashes?

Charged + reserved, then process dies before ship. Without durable progress: don't know whether to forward, retry, or compensate. Fix: record completed steps in a crash-safe store; resume or compensate. Workflow engines do this via history + replay.

How do you update running workflows?

10,000 loan approvals in flight; add compliance check. Versioning: new code for new runs; old runs finish on old version. Migration: in-place update — Step Functions pins in-flight executions to start definition; Temporal patched() branches deterministically so replay stays valid.

if (workflow.patched("change-behavior")) {
  await a.newBehavior();
} else {
  await a.legacyBehavior();
}

Workflow state size / history growth

Minimize Activity payload sizes (pass IDs, not blobs). Continue-as-New (Temporal): snapshot state, start fresh run with empty history — standard for long-lived per-user workflows.

External events & long waits

Signals + durable timers — workflow holds no thread while customer takes 5 days to sign. Webhook calls engine API with workflow ID to wake the right execution.

Exactly-once activity effects?

Engines retry Activities when success report is lost → at-least-once attempts, exactly-once effect via idempotency keys. Mark IN_PROGRESS before irreversible action, COMPLETED after; reconcile rare stuck IN_PROGRESS. True exactly-once delivery impossible across network — idempotency is the answer.

In your interview

"This is a multi-step saga — I'll model states with compensations: refund if inventory fails, release if ship fails. MVP: orchestrator service + orders table + idempotency keys. If we need multi-day human waits or webhook-heavy flows, I'll use Temporal (or Step Functions on AWS) for durable execution and signals. Every Activity gets an idempotency key; compensations retry too. I'll name choreography if teams own services independently, orchestration if the flow is complex and central visibility matters."

Saga storyboard: UPI checkout

Cost and performance levers

Interview Q&A by level

Practice saying these out loud for sagas / multi-step processes. Interviewers grade clarity and judgment more than buzzwords.

Interview takeaway

Match depth to the bar: define → trade off → operate. Don't dump principal answers in an entry-level screen.

Wrapping up

Workflow systems fit hairy state machines that are otherwise painful to get right. They centralize persistence, retries, and compensation so business logic reads like requirements — not infrastructure gymnastics.

Recognize when you're hand-rolling what an engine provides: crash-safe progress, multi-service orchestration, long waits, audit trails. If you're building sagas in Redis if/else, consider graduating.

Continue with Common patterns, Message queues, Kafka, Contention, Sharding (sagas over 2PC), and Agentic architectures.

← Lattice