You've picked SAGA - now you need an implementation
If a distributed transaction needs to touch order, payment, and inventory services and a single
@Transactional boundary isn't an option, a SAGA - a sequence of local transactions with
compensating actions for rollback - is the standard fix. Deciding that is only step one. The next
decision is how each step in the SAGA finds out it's its turn: does every service just listen for events
and react, or does one service tell everyone else exactly what to do and when? These are choreography and
orchestration, and they produce systems with very different failure modes, even when they implement the
exact same business flow.
Choreography: no one is in charge
In a choreographed SAGA, there's no central coordinator. Each service publishes an event when it finishes
its local transaction, and the next service in line is simply subscribed to that event and reacts to it.
Order Service publishes OrderCreated. Payment Service, listening for that event, charges the
card and publishes PaymentCompleted. Inventory Service, listening for that, reserves
stock and publishes InventoryReserved. No service knows the whole flow - each one only knows
"when I see event X, do Y and emit event Z."
// Payment Service - reacts to an event it doesn't control the timing of
eventBus.subscribe('OrderCreated', async (event) => {
try {
const charge = await paymentGateway.charge(event.orderId, event.amount);
await eventBus.publish('PaymentCompleted', { orderId: event.orderId, chargeId: charge.id });
} catch (err) {
await eventBus.publish('PaymentFailed', { orderId: event.orderId, reason: err.message });
}
});What choreography gets you
No single point of failure for coordination logic. Services stay loosely coupled - Payment Service has zero knowledge that Inventory Service even exists. Adding a new step (say, a fraud-check service) means subscribing it to an existing event - nothing else has to change.
What it costs you
There is no single place that shows you the state of an in-flight SAGA. Answering "where is order #4471 right now?" means tailing logs or event streams across four services. Cyclical dependencies between event subscriptions are easy to introduce by accident and hard to spot in a code review.
Orchestration: one service calls the shots
In an orchestrated SAGA, a dedicated orchestrator service owns the entire flow as an explicit state machine. It calls Payment Service directly (or via command messages), waits for the result, then calls Inventory Service, and so on - deciding at each step whether to proceed or start issuing compensations. Participant services don't know about each other at all; they only know about the orchestrator.
// Order Saga Orchestrator - owns the whole flow as an explicit state machine
class OrderSagaOrchestrator {
async run(orderId) {
const saga = await this.sagaStore.create(orderId, 'STARTED');
try {
await this.paymentClient.charge(orderId);
await this.sagaStore.update(orderId, 'PAYMENT_DONE');
await this.inventoryClient.reserve(orderId);
await this.sagaStore.update(orderId, 'INVENTORY_DONE');
await this.shippingClient.schedule(orderId);
await this.sagaStore.update(orderId, 'COMPLETED');
} catch (err) {
await this.compensate(orderId, saga.currentStep);
}
}
async compensate(orderId, failedAtStep) {
// Walk backwards, only undoing steps that actually completed.
if (failedAtStep >= 'INVENTORY_DONE') await this.inventoryClient.release(orderId);
if (failedAtStep >= 'PAYMENT_DONE') await this.paymentClient.refund(orderId);
await this.sagaStore.update(orderId, 'COMPENSATED');
}
}What orchestration gets you
One place to look for the current state of any SAGA instance - the orchestrator's state store. Compensation logic lives in one file instead of being scattered across every participant's event handlers. Adding conditional branching (skip fraud check for repeat customers, say) is a straightforward if-statement rather than a rearrangement of event subscriptions.
What it costs you
The orchestrator becomes a service every other service depends on, and a bug in it can stall every in-flight SAGA at once. It also needs its own durable state store and its own recovery logic for what happens if the orchestrator crashes mid-flow - that's real infrastructure, not a config option.
The decision, as a table
Question Favors ----------------------------------------- ------------------ 3+ services, complex branching logic? Orchestration Need a single dashboard/status view? Orchestration Steps mostly linear, 2-3 services? Choreography New participants added frequently? Choreography Team wants explicit, testable state logic? Orchestration Want to avoid a new coordinator service? Choreography
In practice, most teams start with choreography because it feels lighter to build - no new service, just event subscriptions - and migrate to orchestration once the SAGA grows past four or five steps and debugging "why is this order stuck" starts eating real engineering time. That migration is a rewrite, not a refactor, so it's worth erring toward orchestration earlier than instinct suggests if you already know the flow has more than a couple of branches.
A hybrid that works: orchestrate the SAGA, publish events anyway
These aren't mutually exclusive. A common production pattern is an orchestrator that owns the flow's
control logic but still publishes domain events (OrderCompleted, OrderCancelled)
for other, unrelated systems - analytics, notifications, audit logging - to consume via choreography.
The orchestrator coordinates the transactional steps; everything downstream that doesn't need to
participate in the transaction just listens.
Key takeaways
- Choreography: each service reacts to events with no central coordinator - loosely coupled, but no single place to see SAGA state.
- Orchestration: a dedicated service owns the flow as an explicit state machine - centralized visibility and compensation logic, at the cost of a new critical-path service.
- Choreography suits short, linear flows with frequently added participants. Orchestration suits flows with branching logic or a hard requirement for status visibility.
- Both approaches still need idempotent step handlers and durable compensation logic - the coordination style doesn't change that requirement.
- A hybrid - orchestrated core transaction, choreographed side effects - is common and often the pragmatic answer.
Zetrixweb