Orchestration Saga Pattern
Orchestration Saga Pattern
"In the orchestration approach, an orchestrator (object) tells the participants what local transactions to execute." — Chris Richardson, microservices.io
Intent
The Orchestration Saga is a centralised approach to distributed transaction coordination. A dedicated orchestrator (a Temporal workflow function in TypeScript, or an Axon @Saga class in Java) coordinates the full sequence of steps. The orchestrator knows all participants and explicitly invokes each step in order. On failure, it executes compensating actions in reverse (LIFO) order.
The Temporal TypeScript SDK does not have a built-in Saga class — the compensation stack pattern is hand-rolled inside a workflow function using a LIFO array of compensation callbacks (compensations.unshift(fn) before each forward step, iterated in the catch block). Temporal's durable execution guarantee means the workflow state survives crashes and restarts, so the compensation array is persisted implicitly in workflow history. The Temporal Java SDK (io.temporal.workflow.Saga) provides an equivalent built-in helper, but this vault uses Axon Framework as the Java reference for event-driven orchestration patterns (established in Phase 12).
Axon Framework provides first-class Saga support with annotation-driven lifecycle management: @Saga on the class, @StartSaga on the first event handler, @SagaEventHandler(associationProperty = "orderId") on subsequent handlers, @EndSaga on the success terminal, and SagaLifecycle.end() for failure termination. The CommandGateway is the injection point for dispatching compensation commands to participating services.
Temporal TypeScript SDK API verification flag
Temporal TypeScript SDK API signatures (
proxyActivities,startToCloseTimeout) verified against docs.temporal.io as of v1.14.x (March 2026). Verify against current docs before production use. The@temporalio/workflowpackage version at time of writing: 1.14.0 (npmjs.com/@temporalio/workflow).
When NOT to Use
- Two-service flows — the overhead of a saga orchestrator (workflow state, compensation tracking, durable execution infrastructure) exceeds the benefit; use a simple retry-with-compensation call in the caller
- Simple event-driven reactions with no strict ordering — if services can react independently to events and no step sequence is required, use choreography; it has lower initial complexity
- When all services are in the same deployment unit — use a single local transaction with database-level rollback instead of a distributed saga
When to Use
- 3+ service workflows with branching logic or strict step ordering — the orchestrator enforces that step B executes only after step A completes; choreography cannot guarantee this across service boundaries
- Complex rollback requiring coordinated compensation in LIFO order — the orchestrator tracks which steps completed and compensates them in reverse order; this is harder to reason about in a choreography event mesh
- When workflow visibility is critical — the orchestrator provides a single point of observation for the current saga state; Temporal's web UI shows workflow history; Axon's tracking processor shows saga state
How It Works
The orchestrator coordinates the saga by invoking activities (Temporal) or dispatching commands (Axon) in sequence. Each forward step registers its compensation before executing — this is the critical invariant: registration precedes execution so that a failure immediately after a step completes does not leave an unregistered compensation.
In Temporal TypeScript, the compensation stack is an array of () => Promise<void> functions. Each step calls compensations.unshift(compensationFn) before await forwardActivity(). The catch block iterates the array and calls each compensation — because unshift prepends, iteration is in LIFO order automatically.
In Axon Java, the @Saga class reacts to domain events. When a step succeeds (e.g., PaymentReservedEvent), the orchestrator dispatches the next command (ReserveInventoryCommand). When a step fails (e.g., InventoryReservationFailedEvent), the saga dispatches compensation commands (ReleasePaymentCommand) to undo earlier steps and calls SagaLifecycle.end().
Temporal provides durable execution: if the workflow crashes, Temporal replays workflow history to reconstruct in-memory state. This means the compensation array is rebuilt from history — activities that already completed are not re-executed (Temporal skips them based on history). PRODUCTION: wrap the compensation loop in CancellationScope.nonCancellable() to ensure compensations run even if the workflow is cancelled mid-execution.
Sequence Diagram
sequenceDiagram
participant O as Saga Orchestrator
participant Pay as Payment Service
participant Inv as Inventory Service
participant Ship as Shipping Service
Note over O,Ship: Happy Path (forward steps)
O->>Pay: ReservePaymentCommand
Pay-->>O: PaymentReservedEvent
Note right of O: Register compensation:<br/>releasePayment
O->>Inv: ReserveInventoryCommand
Inv-->>O: InventoryReservedEvent
Note right of O: Register compensation:<br/>releaseInventory
O->>Ship: ShipOrderCommand
Ship-->>O: ShipmentFailedEvent
Note over O,Ship: Failure: compensate in LIFO order
rect rgb(255, 230, 230)
O->>Inv: ReleaseInventoryCommand [idempotent]
Inv-->>O: InventoryReleasedEvent
O->>Pay: ReleasePaymentCommand [idempotent]
Pay-->>O: PaymentReleasedEvent
end
Note over O: Orchestrator tracks all state:<br/>single point of observation
Axon
associationPropertypitfallEvery
@SagaEventHandlermust specifyassociationPropertyto route events to the correct saga instance. Axon routes incoming events to saga instances by matching the event field named inassociationProperty(e.g.,orderId) to the saga's association map. IfassociationPropertyis missing, Axon cannot route the event — it throws a routing exception at runtime or delivers the event to no saga instance at all.This is the most common Axon Saga implementation error. The first
@StartSagahandler establishes the association; every subsequent@SagaEventHandlermust declare the sameassociationPropertyto continue receiving events for the same saga instance.
TypeScript Example
// Orchestration Saga — TypeScript (Temporal SDK @temporalio/workflow v1.14.x)
// Source: temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { reservePayment, reserveInventory, confirmOrder,
releasePayment, releaseInventory } = proxyActivities<typeof activities>({
startToCloseTimeout: '1 minute',
});
type Compensation = () => Promise<void>;
export async function orderSagaWorkflow(orderId: string): Promise<void> {
const compensations: Compensation[] = [];
try {
compensations.unshift(releasePayment.bind(null, orderId));
await reservePayment(orderId);
compensations.unshift(releaseInventory.bind(null, orderId));
await reserveInventory(orderId);
await confirmOrder(orderId);
// success — compensations not needed
} catch (err) {
// PRODUCTION: wrap in CancellationScope.nonCancellable() to ensure compensations run on cancel
for (const compensate of compensations) { // LIFO: unshift builds reverse-order array
await compensate();
}
throw err;
}
}Java Example
// Orchestration Saga — Java (Axon Framework)
// Source: docs.axoniq.io/axon-framework-reference/4.10/sagas/implementation/
@Saga
public class OrderProcessingSaga {
@Autowired private transient CommandGateway commandGateway;
@StartSaga
@SagaEventHandler(associationProperty = "orderId")
public void on(OrderCreatedEvent event) {
commandGateway.send(new ReservePaymentCommand(event.getOrderId(), event.getAmount()));
}
@SagaEventHandler(associationProperty = "orderId")
public void on(PaymentReservedEvent event) {
commandGateway.send(new ReserveInventoryCommand(event.getOrderId()));
}
@SagaEventHandler(associationProperty = "orderId")
public void on(PaymentReservationFailedEvent event) {
// No forward steps to compensate — saga ends with failure
SagaLifecycle.end();
}
@SagaEventHandler(associationProperty = "orderId")
public void on(InventoryReservationFailedEvent event) {
commandGateway.send(new ReleasePaymentCommand(event.getOrderId())); // compensate step 1
}
@EndSaga
@SagaEventHandler(associationProperty = "orderId")
public void on(OrderConfirmedEvent event) { /* saga complete */ }
}Anti-pattern: Non-idempotent compensation
Compensation activities may be retried by Temporal (at-least-once execution guarantee); compensation commands may be retried by Axon. Both must be idempotent. A non-idempotent compensation executed twice causes data corruption: releasing a payment reservation twice may produce a negative balance or a double-credit.
Prefer semantic idempotency:
UPDATE SET status='available' WHERE status='reserved' AND id=?rather than an unconditional state change. Design compensation commands so that repeated execution is a no-op when the target is already in the compensated state.See Idempotent-Consumer for the full deduplication strategy catalogue.
Lineage Backward
- Domain-Events — the orchestrator reacts to domain events to advance the workflow (Axon
@SagaEventHandleris triggered by domain events; Temporal activities emit events that the workflow reacts to) - Message-Router — Axon Saga's
CommandGatewaydispatches are specialised command routing; the saga is a stateful command router that dispatches different commands based on the current saga state - Idempotent-Consumer — compensation actions are a prerequisite correctness concern; all compensation activities and commands must be idempotent
Lineage Forward
- Compensating-Transactions — worked failure scenario with 3-step flow showing LIFO compensation execution
- Enterprise-Patterns-MOC — the MOC for all enterprise patterns including Saga (Phase 13)
Related Concepts
| Pattern | Relationship |
|---|---|
| Choreography-Saga-Pattern | Sibling saga variant — decentralised event chain instead of central coordinator; preferred for simple, loosely-coupled flows |
| Compensating-Transactions | Compensation mechanism that applies to orchestration sagas; worked failure scenario with both variants |
| Domain-Events | Orchestrator reacts to domain events to advance state; Axon @SagaEventHandler is an event-driven saga state machine |
| Idempotent-Consumer | Compensation prerequisite — all compensation activities must tolerate repeated execution |
| Message-Router | Axon CommandGateway is a specialised command router dispatching to the correct aggregate/service |
| CQRS-Pattern | CQRS command bus (write side) is the recipient of compensation commands dispatched by the orchestrator |
Sources
- microservices.io/patterns/data/saga.html — Chris Richardson, Saga pattern, orchestration variant definition
- temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas — Temporal official blog: TypeScript compensation stack code pattern with
unshift+ LIFO - docs.temporal.io/develop/typescript/core-application — Temporal TypeScript SDK:
proxyActivities, workflow function, Node.js 18+ requirement - docs.axoniq.io/axon-framework-reference/4.10/sagas/implementation/ — Axon Framework Saga annotations:
@Saga,@StartSaga,@SagaEventHandler(associationProperty),@EndSaga,SagaLifecycle.end() - Chris Richardson, Microservices Patterns, 2018 — Saga chapter, orchestration mechanics, compensating transactions
- Temporal Java SDK alternative: docs.temporal.io/develop/java —
io.temporal.workflow.Sagaclass withaddCompensation()andcompensate()methods