Under the Hood
Distributed

Sagas: orchestration vs choreography

When a single business action spans several services — book flight, charge card, reserve hotel — you can't wrap it in one transaction, so you use a saga: a sequence of local transactions, each with a compensating action that undoes it if a later step fails. This lesson goes past 'what a saga is' into how you actually coordinate one: choreography (services react to each other's events, no central brain) versus orchestration (one coordinator explicitly drives the steps), the real trade-offs between them, why compensations aren't rollbacks, and the failure that has no clean answer — the compensation that itself fails.

Sagas: orchestration vs choreography

The conflict-resolution lesson introduced the saga as the alternative to a distributed transaction: instead of one atomic operation across services (which needs the blocking, fragile two-phase commit), you run a sequence of local transactions, each in its own service, and if a later step fails you execute compensating transactions that semantically undo the earlier ones. That's the what. This lesson is the how — because once you commit to sagas, the real question is how the steps are coordinated, and there are two fundamentally different answers with very different consequences.

First, one point worth nailing down because it trips everyone: a compensating transaction is not a rollback. A rollback un-does an uncommitted transaction — as if it never happened. But in a saga each step already committed in its own service; you cannot un-commit it. So a compensation is a new transaction that semantically reverses the effect: you don't un-charge the card, you issue a refund; you don't un-reserve the hotel, you cancel the reservation. The effect is neutralized, but the history shows both the action and its reversal — and some actions can't be fully reversed (an email that was sent, a notification that was seen), which is a design constraint you have to live with, not a bug to fix.

Choreography: services react to events, no central brain

In choreography, there is no coordinator. Each service listens for events and reacts by doing its local work and emitting its own event, and the saga emerges from that chain of reactions. A checkout saga, choreographed:

  1. Order service creates the order, emits OrderCreated.
  2. Payment service hears OrderCreated, charges the card, emits PaymentCompleted.
  3. Inventory service hears PaymentCompleted, reserves stock, emits StockReserved.
  4. Shipping service hears StockReserved, schedules delivery.

And on failure, the compensations flow the same way in reverse: if inventory can't reserve, it emits StockReservationFailed, the payment service hears it and issues a refund, the order service hears that and cancels the order. It's event-driven architecture all the way down — nobody's in charge, the flow is the sum of who-reacts-to-what.

The appeal is maximum decoupling: no service knows about the others, you add a step by adding a subscriber, and there's no central component to build or bottleneck on. The cost is there is no single place that knows what the saga is doing. The flow is scattered across services' event subscriptions, so understanding "what happens when you check out" means tracing events through five codebases; debugging a stuck saga means reconstructing its state from logs across services; and it's easy to accidentally create cyclic event dependencies or have no idea which sagas are mid-flight. Choreography is elegant for short, simple, loosely-coupled flows and becomes an untraceable web for complex ones.

Orchestration: one coordinator directs the steps

In orchestration, a central orchestrator (the saga coordinator) explicitly drives the flow: it calls service A, waits for the result, calls B, and on any failure invokes the compensations in reverse order. The saga's logic — the sequence, the branches, the compensations — lives in one place, as an explicit state machine:

orchestrator:
  charge payment      → ok? next : abort
  reserve inventory   → ok? next : compensate(refund payment); abort
  schedule shipping   → ok? done : compensate(release inventory, refund payment)

The appeal is visibility and control: one component knows the entire flow and the current state of every in-flight saga, error handling and compensation order are explicit, and debugging is "look at the orchestrator." The cost is that the orchestrator is a real component you build, run, and keep available, and if you're not careful it becomes a god-object that accretes all the business logic while the services degrade into anemic CRUD wrappers. Orchestration is the right call for complex, many-step flows where you need to see what's happening and handle failure deliberately — which, in practice, is most sagas that matter.

The honest rule of thumb: choreography for simple flows where decoupling is the priority; orchestration for complex flows where visibility and explicit failure-handling are the priority. Many real systems mix them — choreograph the loose coupling between bounded contexts, orchestrate the intricate flow within one.

Run the checkout saga and choose where it fails. Fail inventory and the payment is refunded; fail shipping and both inventory and payment unwind — compensations fire in reverse, one per already-committed step. There is no cross-service ROLLBACK; the coordinator rebuilds consistency by hand.

Make this step fail
1Charge paymentpending
2Reserve inventorypending
3Schedule shippingpending

A saga is a sequence of local transactions with no shared rollback — so when a step fails, there's nothing to ROLLBACK; the coordinator must run a compensating action for each already-committed step, in reverse order. Fail the inventory step and watch the payment get refunded; fail shipping and both inventory and payment unwind. That explicit, ordered cleanup — visible in one place — is exactly what orchestration buys you over event choreography.

Designing for no isolation

Sagas give up the I in ACID: because each step commits independently, the intermediate states are visible to everyone else while the saga is mid-flight (for a moment the card is charged but the hotel isn't booked). You can't hide that, so you design for it — mark the affected records with an explicit in-progress state (pending, reserving) so other operations and users know this thing is mid-saga and not yet final. This is the same instinct as the settlement state machine's marked_paid — a visible, honest "in between" state — rather than pretending the operation is instantaneous.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. Why is a compensating transaction not a rollback? Give a concrete example of a compensation, and explain what "some actions cannot be fully reversed" means for saga design.
  2. Describe a choreographed saga and an orchestrated saga for the same checkout flow. What does each optimize for, and what does each make hard?
  3. State the rule of thumb for choosing choreography vs orchestration, and explain the specific reason (not performance) you would migrate a growing choreographed saga to orchestration.
  4. Sagas give up isolation. Explain what that means for intermediate state, and how you design for the fact that a half-finished saga is visible to everyone.
  5. A saga completes three forward steps, the fourth fails, and a compensation for step one then also fails. Describe the state the system is in and the three things that make compensations robust enough to survive it.
  6. Fable has no cross-service saga but its settlement flow is saga-shaped. Identify the orchestrator, the visible mid-saga state, and the compensating actions, and say why running inside one database lets it use transactions instead.