Run saga steps in order, persist progress after each one, compensate completed steps in reverse on failure, retry compensations, and resume from the store after a crash without redoing work.
An order needs three services: reserve the stock, charge the card, book the courier. There's no transaction across them, so the order is a saga: a sequence of local transactions, each with a compensating transaction that semantically undoes it (Day 2, sections 2.1 to 2.3). Garcia-Molina and Salem's "Sagas" (SIGMOD 1987) states the promise: either every step T1…Tn runs, or T1…Tj run followed by their compensations Cj…C1. Build the orchestrator that keeps that promise (sections 3.2 and 4.2): it calls the steps, records progress in a durable store, compensates on failure, and survives its own crashes.
SagaOrchestrator(store, steps, max_compensation_attempts=5), where store is Given's SagaStore and steps a list of Given's Step(name, action, compensate):
| Method | Does |
|---|---|
start(saga_id) |
saves a new saga with status "started". Raises ValueError if the id already exists |
run(saga_id) |
runs or resumes the saga from its saved record until it ends, and returns the final status. On a finished saga it calls nothing and returns its status |
status(saga_id) |
the status in the store, or None for an unknown id |
recover() |
runs every saga in the store whose status is "started", "executing" or "compensating", in store.ids() order, and returns {saga_id: final status} for them |
Statuses, after the lesson's coordinator (section 4.2): started → executing → completed, or executing → compensating → compensated, or compensating → failed when a compensation can't be made to work (a human takes over; failed is final).
Rules the tests check:
step.action(key) and step.compensate(key) with key = f"{saga_id}/{step.name}", the same key every time, for both. The services are idempotent by key (Day 2, section 8.3), so calling a step again with its key never applies its effect twice.executing before the first action. Run the steps in order; after each action returns, save that it completed before calling the next. When all have completed, save completed.StepFailed, save status compensating, then compensate the steps that completed, in reverse order (the failed step's own transaction failed, so it isn't compensated). After each compensation returns, save that it's done. Then save compensated. A first step that fails leaves nothing to compensate.TransientError is called again, up to max_compensation_attempts calls in all. If the last one fails too, save failed and stop: run no more compensations.Crash means the orchestrator process died right there. Never catch it (catch only StepFailed and TransientError). The test then builds a new orchestrator on the same store and calls run or recover, which must pick up from the saved record: don't run completed steps or done compensations again, re-call the step that was in flight (its key makes that safe), and keep compensating if the saga was compensating.SagaStore checks). Nothing in the orchestrator object survives a crash.In JavaScript and TypeScript it's new SagaOrchestrator(store, steps, maxCompensationAttempts = 5) with start, run, status (null for unknown) and recover(), which returns a plain object. A step is new Step(name, action, compensate), and the key is `${sagaId}/${step.name}`. Every error is caught by a catch, so check with instanceof and rethrow anything that isn't a StepFailed (in actions) or TransientError (in compensations), Crash included.
The chaos tests run four orders through two workers while a "monkey" makes services crash the orchestrator after an action or a compensation, time out compensations, and declines some steps; the scheduler may lose run's reply (so run is called again) or kill a worker, whose sagas a recovery job then resumes. At the end every started saga must be either completed with every effect applied, or compensated with none left applied, and no step's effect may have been applied twice.
What's left out: steps run one at a time in one process (no parallel branches, no timeouts on a slow step); run is synchronous and retries a compensation immediately instead of on a backoff schedule from a background job (Day 2, section 12.2); there are no pivot steps (the tested follow-up) and nothing stops two orchestrators from running the same saga at once (a discussion follow-up). Workflow engines like Temporal (Day 3) keep this kind of progress record for you, as the workflow's event history.
Write your code, then run 3 example tests and 9 hidden tests. Each run is limited to 8 seconds: an infinite loop gets stopped and the runtime restarts, your code stays.