A saga is a sequence of steps across services. Something has to decide which step runs next, and which compensations to run when one fails. There are two ways to do that.
You can have a conductor: one component that knows the whole sequence and tells each service what to do. Or you can have a dance where everyone knows their own moves: each service listens for what just happened and does its part. These are called orchestration and choreography, and choosing between them shapes how easy your saga is to change, debug and trust.
Orchestration
An orchestrator owns the saga. It's usually a small service, or a module inside one, that stores a saga record and drives the steps one by one, by sending commands and waiting for replies.
In orchestration, one component decides each saga's next step. In choreography, each service reacts to events from the others, and the flow emerges from those reactions.
For Tiffin's checkout, the orchestrator lives in a checkout service:
%% caption: Orchestration. One component tells each service what to do, and decides what's next. flowchart TD ORC["Checkout orchestrator<br/>holds the saga state"]:::hot --> OS["Order service<br/>create pending order"] ORC --> PR["Promotions<br/>reserve coupon"] ORC --> PA["Payment<br/>authorise"] ORC --> RE["Restaurant<br/>request acceptance"]
The saga's logic is one readable state machine:
const checkoutSaga = {
steps: [
{ name: 'createOrder', run: orders.createPending, undo: orders.cancel },
{ name: 'reserveCoupon', run: promos.reserve, undo: promos.release },
{ name: 'authorise', run: payments.authorise, undo: payments.void },
{ name: 'kitchenAccept', run: restaurants.requestAccept }, // pivot
{ name: 'capture', run: payments.capture, retryForever: true },
{ name: 'confirm', run: orders.confirm, retryForever: true },
],
}
The orchestrator saves the saga's state (step = 'authorise', status = 'running') to its database after every step, so if it crashes it resumes where it left off. Each call carries an idempotency key made from the saga ID and the step name (so authorise and capture don't share a key), and a resumed step that already ran does nothing new. The try-confirm-cancel lesson later in this module covers crash recovery step by step.
You can build this yourself with a table and a worker, or use a workflow engine that does the bookkeeping for you. Temporal, AWS Step Functions and Camunda are common choices. Temporal, for instance, lets you write the saga as ordinary code with await calls, and it records every step so the code can resume after a crash as if nothing happened.