intermediate~2h

Orchestration Saga

How a single orchestrator component explicitly directs every step of a multi-service business process, holding the full sequence of commands and compensations in one visible place instead of scattering it across independent listeners.

Learning objectives

  • Explain how an orchestration saga centralizes a multi-step process into one component
  • Implement an Axon @Saga class using @StartSaga, @SagaEventHandler, and @EndSaga
  • Explain what associationProperty does and why it matters when many saga instances run concurrently
  • Decide between choreography and orchestration for a given business process based on step count and coupling needs

◆ Story

An orchestra conductor doesn't play a single note. What they hold is the entire score — every section's part, in the correct order — and at each moment they cue exactly the section that's supposed to come in next. If the violins come in wrong, the conductor is the one who decides how the rest of the orchestra responds. No individual musician needs to memorize the whole piece; they just need to watch the conductor and play when cued.

An orchestration saga has exactly this shape. Instead of several services independently reacting to each other's events, one dedicated component — the orchestrator — holds the entire sequence of steps and explicitly tells each participating service what to do next. The participating services stay simple: they receive a command, execute it, and report back with an event. All of the sequencing logic, all of the "what happens next," and all of the failure handling lives in one place.

This is a direct answer to choreography's core weakness. Where a choreography saga's plan only exists as an emergent pattern spread across many independent listeners, an orchestration saga's plan is written down, explicitly, in one class you can open and read top to bottom. Nothing about the individual steps changes — depositing funds and updating a credit profile are still the same operations — but who decides the order, and who decides what happens on failure, moves from "nobody, really" to "this one component, explicitly."

That centralization is a trade-off, not a free upgrade. The orchestrator now needs to know about every participant it coordinates, which is a form of coupling choreography avoided. The right call depends on how many steps are involved and how much that explicit visibility is worth.

Axon Framework has first-class support for exactly this pattern. A class annotated @Saga represents a single running instance of an orchestrator — one object per in-flight process, created automatically when a matching starting event arrives and destroyed automatically once the process ends.

Three annotations do the real work:

  • @StartSaga marks the event handler that creates a new saga instance. When this event is published, Axon instantiates a fresh @Saga object to track this one specific process.
  • @SagaEventHandler marks every other event handler the saga reacts to while it's alive, each one usually issuing the next command in the sequence.
  • @EndSaga marks the handler whose event signals the process is finished, telling Axon it's safe to discard this saga instance.

Every @SagaEventHandler (including the one marked @StartSaga) needs an associationProperty — the name of a field Axon should match against the incoming event to figure out which saga instance the event belongs to.

◆ Under the hood

A real system can have hundreds of loan approvals in progress at once, each one a separate saga instance. When a FundsDepositedEvent arrives, Axon needs to know which of those hundreds of in-flight sagas it belongs to — that's exactly what associationProperty is for. By declaring associationProperty = "accountId", the saga tells Axon: "route this event to whichever saga instance is currently associated with this specific account ID." Get this wrong, or forget to set it, and events can get routed to the wrong saga instance entirely, or fail to find a matching instance at all.

This is meaningfully different from choreography's plain @EventHandler. A saga instance carries its own state (like a loan ID and account ID) across multiple events over time, whereas a stateless @EventHandler reacts to one event with no memory of what came before.

Here is the same loan approval process from before — approve a loan, deposit funds, update a credit profile — implemented as an explicit orchestrator instead of three independent listeners.

Reading this class top to bottom tells you the entire story of the process: it starts on LoanApprovedEvent, sends DepositFundsCommand; reacts to FundsDepositedEvent, sends UpdateCreditProfileCommand; and ends on CreditProfileUpdatedEvent. That's a genuine, tangible payoff of orchestration — the multi-step plan is visible in one place, instead of being spread implicitly across three separate listener classes in three separate services.

Notice what the participating services look like from this saga's point of view: Accounts Service just needs to know how to handle DepositFundsCommand and publish FundsDepositedEvent when it's done. It has no idea it's part of a saga at all — it's just responding to a command like it would respond to any other command. All of the sequencing awareness lives in LoanApprovalSaga, not in the services it coordinates. This is the coupling trade mentioned earlier: the orchestrator now explicitly depends on knowing about Accounts Service, Customer Service, and the commands each one accepts — but in exchange, those services themselves stay simple and saga-agnostic.

The saga's own fields — loanId and accountId — persist between event handler calls for the lifetime of this one saga instance. Axon manages that persistence automatically; you don't need to manually save or load saga state anywhere in this code.

💻 Code example

@Saga public class LoanApprovalSaga { @Autowired private transient CommandGateway commandGateway; private String loanId; private String accountId; @StartSaga @SagaEventHandler(associationProperty = "loanId") public void on(LoanApprovedEvent event) { this.loanId = event.getLoanId(); this.accountId = event.getAccountId(); commandGateway.send(new DepositFundsCommand(event.getAccountId(), event.getLoanAmount())); } @SagaEventHandler(associationProperty = "accountId") public void on(FundsDepositedEvent event) { commandGateway.send(new UpdateCreditProfileCommand(event.getCustomerId(), loanId)); } @EndSaga @SagaEventHandler(associationProperty = "loanId") public void on(CreditProfileUpdatedEvent event) { // Saga finished successfully — Axon automatically ends and cleans it up here. } }

Compensation in orchestration looks structurally similar to choreography's — a command gets sent to undo an earlier step — but who makes that decision is different, and that difference matters.

In choreography, Loans Service itself had to know to listen for DepositFailedEvent and decide, on its own, to send CancelLoanCommand. In orchestration, the saga is the one listening for the failure and the one deciding to compensate. Loans Service doesn't need any awareness that a saga exists at all — it just needs to know how to handle CancelLoanCommand whenever it arrives, the same as any other command.

SagaLifecycle.end() explicitly tells Axon this saga instance is finished — no further steps are expected, and Axon can safely discard its state. This is worth contrasting with the @EndSaga annotation used on the happy path: @EndSaga ends the saga automatically as a side effect of handling a specific event, while SagaLifecycle.end() ends it imperatively, from inside a handler that might also be doing other things, like sending a compensating command first.

The practical benefit shows up when a process has more than one possible failure point. With orchestration, every failure path — and every corresponding compensation — is a handler inside the same class, next to the happy path it's compensating for. Reading the saga tells you not just what should happen, but everything that could go wrong and exactly how each failure gets handled. That's much harder to get a complete picture of in choreography, where each service only knows about the one failure event it personally listens for.

💻 Code example

@SagaEventHandler(associationProperty = "accountId") public void on(DepositFailedEvent event) { commandGateway.send(new CancelLoanCommand(loanId)); // The saga decides to compensate, explicitly. SagaLifecycle.end(); // And explicitly ends itself here — no further steps expected. }

Neither pattern is strictly better — they make different trade-offs, and the right choice depends mostly on how many steps are involved and how much explicit visibility into failure handling actually matters.

ChoreographyOrchestration
Where the plan livesImplicitly, spread across many independent listenersExplicitly, in one saga class
CouplingLooser — services only know about event types, not each otherThe orchestrator knows about every participant; participants stay simple
Best fitA small number of steps, loosely related servicesLonger or more complex flows, or ones needing a clear audit trail
Failure handling visibilityScattered — each service only sees its own compensationCentralized — every failure path lives in one class

▲ Common mistake

Reaching for orchestration by default, even for a genuinely simple two-step process, just because it feels more "proper" or more visible. That's paying real coupling cost — the orchestrator now depends on every participant's command and event types — for a benefit that a two-step choreography chain doesn't actually need. The opposite mistake is just as real: sticking with choreography past the point where the implicit plan has become genuinely hard to trace, out of reluctance to introduce a new component.

A practical rule that holds up well in real systems: start with choreography for a short, loosely coupled flow, and deliberately migrate to orchestration once step count grows past four or five, once compensation logic starts getting complicated, or once someone genuinely needs to answer "what exactly happens during this process, end to end" without spelunking through several codebases.

Orchestration tends to be the right call whenever a process has enough steps, or enough ways to fail, that a human needs to be able to answer "what happens, in order, and what happens if step three fails?" without cross-referencing several services.

  • Long, multi-step approval workflows. Loan origination, KYC (know-your-customer) onboarding, or insurance claim processing often involve five, six, or more steps across different systems, each with its own failure mode and its own compensation. An explicit saga class makes the entire workflow auditable in one place.
  • Processes needing a clear audit trail. Regulated domains — banking, healthcare, insurance — often need to demonstrate exactly what sequence of actions occurred for a given transaction. A saga's persisted state and explicit event handlers give you that trail almost for free.
  • Flows with several distinct failure paths. When "what happens if X fails" has a different answer depending on which step X is, keeping all of those answers in one file makes the system meaningfully easier to reason about and extend safely.
  • Cross-team coordination that needs a visible contract. Even though the orchestrator is more tightly coupled to its participants, that coupling is explicit and reviewable — a new team member can read one class and understand the entire process, rather than having to discover the flow by grepping for event listeners across several repositories.

Order-to-cash pipelines, multi-party payment settlement, and travel-booking systems (flight plus hotel plus car, each of which can fail independently and needs its own compensation) are all classic real-world domains where orchestration's centralized visibility earns back its coupling cost many times over.

Q: What's the core structural difference between choreography and orchestration? A: In choreography, the plan emerges from many independent listeners reacting to events. In orchestration, one dedicated saga component explicitly holds the entire sequence and tells each participant what to do next.

Q: What does associationProperty do on a @SagaEventHandler? A: It tells Axon which field to match an incoming event against, so the event gets routed to the correct in-flight saga instance among potentially hundreds running concurrently.

Q: In orchestration, who decides to trigger a compensating transaction? A: The orchestrator. It centrally observes the failure event and explicitly issues the compensating command, rather than the failing step's own service deciding on its own.

Q: What do the participating services (like Accounts Service) need to know about the saga coordinating them? A: Nothing. They just handle whatever command they receive and publish the resulting event — all sequencing awareness lives in the orchestrator, not in the participants.

Q: When does it typically make sense to migrate from choreography to orchestration? A: Once a process grows past roughly four or five steps, once compensation logic gets complicated, or once someone needs a single, readable place to understand the entire flow and its failure handling.

Want a visual for this concept?

Generate a diagram tailored to “Orchestration Saga” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Subscription Queries & Event Replay← Back to all Event-Driven Microservices chapters