👋Hi, I'm Waqas — a Software Architect and Technical Consultant specializing in .NET, Azure, microservices, and API-first system design..
I help companies build reliable, maintainable, and high-performance backend platforms that scale.
Saga Pattern: Orchestrator vs Choreography in Microservices
Saga pattern: orchestrator vs choreography, when to use which, and .NET examples.
September 3, 2025 · Waqas Ahmad
Read the article
Introduction
This guidance is relevant when the topic of this article applies to your system or design choices; it breaks down when constraints or context differ. I’ve applied it in real projects and refined the takeaways over time (as of 2026).
Cross-service business operations span multiple services and databases, so you cannot use a single ACID transaction and need a pattern that keeps consistency through local transactions and compensation when a step fails. The Saga pattern is exactly that: a sequence of local transactions, each with a compensating action that undoes it if a later step fails. This article explains the Saga pattern in depth, then Orchestrator and Choreography with full code, when to use which, and best practices—for architects and tech leads, choosing orchestrator for complex flows and choreography for simple linear flows keeps state and recovery manageable.
System scale: Varies by context; the approach in this article applies to the scales and scenarios described in the body.
Team size: Typically small to medium teams; ownership and clarity matter more than headcount.
Time / budget pressure: Applicable under delivery pressure; I’ve used it in both greenfield and incremental refactors.
Technical constraints: .NET and related stack where relevant; constraints are noted in the article where they affect the approach.
Non-goals: This article does not optimize for every possible scenario; boundaries are stated where they matter.
What is the Saga pattern and why it matters
The problem: A business flow like “Place order” may need to reserve inventory (Inventory service), charge payment (Payment service), and create shipment (Shipping service). Each service has its own database. You cannot run a single ACID transaction across three databases, so either you lock across services (expensive, brittle) or you accept eventual consistency and undo (compensate) when a step fails.
The Saga pattern: A Saga is a sequence of local transactions. Each step:
Performs a local transaction (e.g. reserve inventory in the Inventory DB).
Then triggers the next step (e.g. send a command or publish an event).
If any step fails, you run compensating transactions in reverse order to undo the effect of previous steps (e.g. release inventory, refund payment, cancel shipment). There is no distributed lock; consistency is eventual and achieved through compensation.
Why it matters: Without a Saga (or similar), a failure in step 2 or 3 leaves step 1 committed with no automatic undo—inconsistent state. With a Saga, you always have a compensating action for each step, so you can restore a consistent state when something fails.
Saga at a glance
Concept
What it is
Local transaction
One step in the Saga; commits in one service’s database only.
Compensating transaction
Undoes the effect of a local transaction (e.g. Release reservation, Refund, Cancel).
Orchestrator
A central coordinator that sends commands to each service and runs compensation commands in reverse on failure.
Choreography
No central coordinator; each service subscribes to events, performs its local transaction, and publishes the next event; on failure it publishes a compensation event and others compensate.
Saga state
Tracks which steps completed (orchestrator holds this; in choreography you infer from events or store per service).
Idempotency
Same command/event processed twice must not double-apply; use idempotency keys and store processed keys.
Saga in detail: compensation, idempotency, failure handling
Compensation: Every forward step must have a compensating action that undoes its effect. Examples: Reserve inventory → Release inventory. Charge payment → Refund. Create shipment → Cancel shipment. Compensation must be idempotent (safe to run twice) and must have enough context (e.g. order ID, amount, reservation ID) to undo correctly.
Idempotency: Messages can be redelivered (retry, crash recovery). If you process the same command twice, you must not double-reserve or double-charge. Use an idempotency key (e.g. SagaId + StepId or client-provided key) and store processed keys (cache or DB). If the key was already processed, skip or return success. Apply the same to compensation (e.g. key release-{SagaId} so double-release is a no-op).
Failure handling: When a step fails (exception, timeout, or business rejection), the Saga runs compensations for all completed steps in reverse order. In orchestrator: the coordinator catches the failure and sends compensation commands. In choreography: the failing service publishes a compensation event; each service that already ran subscribes and runs its compensation.
Ordering: Compensations must run in reverse order of the forward steps. Step 1 → Step 2 → Step 3; on failure: compensate 3, then 2, then 1.
State storage: The orchestrator must persist Saga state (SagaId, OrderId, Status, CompletedStepIndex) so that after a crash it can resume or compensate. Use a state store (DB or durable queue). In choreography, each service may store only what it needs for its own compensation; there is no single “Saga state” unless you add a separate tracking service.
Visibility: In orchestrator, one place holds the flow—easy to monitor and debug. In choreography, use correlation ID (same ID on all events for one Saga) and distributed tracing to follow the flow.
Timeout: Define a timeout per step (or for the whole Saga). If a step does not complete in time, treat it as failed and run compensations.
Orchestrator saga: how it works and full code
What it is: A central coordinator (orchestrator service) drives the Saga. It holds Saga state, sends commands to each participant (e.g. ReserveInventory, ChargePayment, CreateShipment), and on failure sends compensation commands in reverse order (CancelShipment, RefundPayment, ReleaseInventory).
How it works:
Client starts the Saga (e.g. POST place order).
Orchestrator creates Saga state (SagaId, OrderId, Status = Running, CompletedStepIndex = -1) and saves it.
Orchestrator sends ReserveInventoryCommand to Inventory service; waits for success (or message ack).
On success, updates state (CompletedStepIndex = 0), sends ChargePaymentCommand to Payment service.
On success, updates state (CompletedStepIndex = 1), sends CreateShipmentCommand to Shipping service.
On success, updates state (Status = Completed). Saga done.
If any step fails: Orchestrator sets Status = Compensating and sends compensation commands in reverse (CancelShipment if step 2 completed, RefundPayment if step 1 completed, ReleaseInventory). Then Status = Failed.
Pros: Single place to understand and debug the flow; easy to add steps and conditional logic. Cons: Orchestrator can be a bottleneck and single point of failure; must be resilient (state store, idempotent handling).
Diagram: Forward commands (solid); on failure, compensations (dashed) in reverse order: Cancel → Refund → Release.
Loading diagram…
In the diagram, solid arrows are the forward commands (ReserveInventory → Inventory, ChargePayment → Payment, CreateShipment → Shipping); dashed arrows are the compensations (Release → Inventory, Refund → Payment, Cancel → Shipping), sent in reverse order when a step fails.
How this fits together: The orchestrator sends commands (e.g. via Azure Service Bus or RabbitMQ). Each participant (Inventory, Payment, Shipping) has a handler that checks idempotency, runs the local transaction, and marks the key processed. The orchestrator waits for success (or uses request-response or correlation). On exception, the orchestrator sends compensation commands in reverse; each participant has a compensation handler (e.g. ReleaseInventoryHandler) that is also idempotent.
Choreography saga: how it works and full code
What it is: There is no central coordinator. Each service subscribes to events and performs its local transaction; then it publishes the next event. If a step fails, it publishes a compensation event; other services that already ran subscribe and run their compensation (in reverse order by design—each knows its own compensation and reacts to the right event).
How it works:
Order service (or API) publishes OrderCreated (OrderId, Lines, Amount, Address).
If Payment fails: Payment publishes PaymentFailed (OrderId). Inventory subscribes to PaymentFailed and runs ReleaseInventory. (Shipping never ran, so no compensation.)
If Shipping fails: Shipping publishes ShipmentFailed (OrderId). Payment subscribes and runs Refund; Inventory subscribes (or a separate OrderFailed event) and runs Release.
Pros: No single point of failure; decentralized; services are loosely coupled. Cons: Flow is harder to see (no single place); adding a step may require changes in multiple services; ordering of compensations must be designed (who listens to which failure event).
How this fits together: Each service subscribes to the events it cares about (e.g. Inventory subscribes to OrderCreated and PaymentFailed). On OrderCreated, it reserves and publishes InventoryReserved. On PaymentFailed (or ShipmentFailed if you design it so Inventory compensates on that too), it runs Release. Idempotency is still required (e.g. use OrderId + event type as idempotency key) so redelivery does not double-apply.
Comparison: Orchestrator vs Choreography
Aspect
Orchestrator
Choreography
Control
Central coordinator sends commands.
Each service reacts to events; no central control.
Flow visibility
One place (orchestrator) holds the full flow.
Flow is distributed; use correlation ID and tracing.
Adding a step
Add step in orchestrator + one new participant.
Add new event + handler in one or more services.
Conditional logic
Easy (orchestrator branches).
Harder (multiple events or routing).
Failure handling
Orchestrator runs compensations in reverse.
Failing service publishes compensation event; others subscribe.
Single point of failure
Orchestrator (mitigate with state store + resilience).
No single point; but harder to debug.
Coupling
Participants depend on commands (contract).
Services depend on events (contract).
Best for
Complex flows, many steps, conditional logic.
Simple linear flows, decentralized teams.
When to use which
Use Orchestrator when:
The flow has many steps or conditional logic (e.g. if payment fails, notify user; if inventory low, waitlist).
You want a single place to monitor, debug, and visualize the Saga.
You are okay with a central component that must be highly available and hold state.
Use Choreography when:
The flow is simple and linear (A → B → C).
You want no central coordinator and decoupled services.
Multiple teams own different services and you prefer event-driven contracts.
In practice: Many teams use orchestrator for critical flows (e.g. order placement, payment) and choreography for simpler or secondary flows (e.g. send email on order created, update analytics).
Idempotency on every forward and compensation handler (key + store).
Persist Saga state in the orchestrator so you can resume or compensate after crash.
Design compensation with the same care as forward steps (undo exactly, no side effects).
Use correlation ID (SagaId) on all commands/events for tracing.
Set timeouts per step and for the whole Saga.
Pitfalls:
Forgetting idempotency — Redelivery causes double-reserve or double-charge.
Compensation that can fail — Use retries, dead-letter, and manual intervention; design for compensation failure.
Partial visibility in choreography — No single place “owns” the Saga; invest in correlation and tracing.
Mixing orchestrator and choreography in one Saga — Pick one style per flow for clarity.
Summary
Saga is a sequence of local transactions with compensating transactions in reverse on failure—no distributed lock, eventual consistency. Choosing orchestrator for complex flows and choreography for simple linear flows gives you one place to reason about state or keeps coupling loose; skipping idempotent compensation leads to double-undo and broken recovery. Next, map your cross-service flow to steps and compensations, then implement with durable orchestrator state (or equivalent in choreography) and idempotency keys on every handler.
Saga = sequence of local transactions with compensating transactions in reverse on failure. No distributed lock; eventual consistency.
Orchestrator: Central coordinator sends commands, holds state, runs compensation commands in reverse. Best for complex flows and single place to debug.
Choreography: No coordinator; services subscribe to events, run local transaction, publish next event; on failure publish compensation event, others compensate. Best for simple linear flows and loose coupling.
Compensation must be idempotent and safe to retry. Idempotency keys on every handler.
Full code: Contracts (commands/events), Saga state, orchestrator (RunOrderSagaAsync, ExecuteStepAsync, compensation in reverse), participant handlers (forward + compensation) with idempotency; choreography handlers (OrderCreated → InventoryReserved → …; PaymentFailed → Release).
Position & Rationale
I use orchestrator when the saga has multiple steps, branching, or a clear need for one place to hold state and run compensation in reverse. I use choreography when the flow is simple and linear and we want to avoid a central coordinator. I insist on idempotent compensation and idempotency keys on every participant handler so retries and replays don’t double-compensate or double-apply. I avoid distributed locks; saga gives eventual consistency and we design for that. I reject sagas when a single transactional boundary can do the work—saga is for cross-service, multi-step flows where we accept eventual consistency and compensating actions.
Trade-Offs & Failure Modes
What this sacrifices: Some simplicity, extra structure, or operational cost depending on the topic; the article body covers specifics.
Where it degrades: Under scale or when misapplied; early warning signs include drift from the intended use and repeated workarounds.
How it fails when misapplied: Using it where constraints don’t match, or over-applying it. The “When I Would Use This Again” section below reinforces boundaries.
Early warning signs: Team confusion, bypasses, or “we’re doing X but not really” indicate a mismatch.
What Most Guides Miss
Most guides explain the two patterns and maybe draw a diagram. They rarely say: compensation is the hard part—you have to design every step’s undo and what happens when the undo itself fails. Ordering and idempotency in choreography: events can arrive out of order or twice; if you don’t design for that, you get duplicate charges or inconsistent state. And when to pick which: orchestrator when you want one place to see the flow and enforce timeouts; choreography when you’re okay with distributed visibility and want less coupling. Most posts don’t give you that decision, they just show both and leave you guessing.
Decision Framework
If the context matches the assumptions in this article → Apply the approach as described; adapt to your scale and team.
If constraints differ → Revisit Decision Context and Trade-Offs; simplify or choose an alternative.
If you’re under heavy time pressure → Use the minimal subset that gives the most value; expand later.
If ownership is unclear → Clarify before scaling the approach; unclear ownership is an early warning sign.
The article body and Summary capture the technical content; this section distils judgment.
Apply the approach where context and constraints match; avoid over-application.
Trade-offs and failure modes are real; treat them as part of the decision.
Revisit “When I Would Use This Again” when deciding on a new project or refactor.
Need help designing resilient microservices? I support teams with domain boundaries, service decomposition, and distributed systems architecture.
When I Would Use This Again — and When I Wouldn’t
I would use the saga pattern again for cross-service, multi-step flows where we accept eventual consistency and compensating transactions—order placement, booking, sign-up with side effects. I’d choose orchestrator when the flow has branching or we want one place to hold state and run compensation; choreography when the flow is simple and linear and we want no central coordinator. I wouldn’t use saga when a single service or single DB transaction can do the work—saga adds complexity and eventual consistency we may not need. I wouldn’t implement saga without durable orchestrator state (or equivalent in choreography) and idempotent compensation; otherwise recovery and retries break. If the team can’t own compensation logic and testing (including failure injection), I’d simplify the flow or add that capability before going full saga.
Frequently Asked Questions
Frequently Asked Questions
What is a Saga?
A Saga is a sequence of local transactions across multiple services. Each step commits in one service; if a later step fails, compensating transactions run in reverse order to undo previous steps. No distributed lock; consistency is eventual via compensation.
What is the difference between Orchestrator and Choreography?
Orchestrator: A central coordinator sends commands to each service and runs compensation commands in reverse on failure. Choreography:No central coordinator; each service reacts to events, runs its local transaction, and publishes the next event; on failure it publishes a compensation event and others compensate.
When use Orchestrator?
Use when the flow is complex (many steps, conditional logic) or you want a single place to monitor and debug. Orchestrator holds state and drives the flow.
When use Choreography?
Use when the flow is simple and linear and you want no central coordinator and loose coupling. Each service reacts to events and publishes the next.
What is compensation?
A compensating transactionundoes the effect of a forward step (e.g. Release reservation, Refund payment, Cancel shipment). Must be idempotent and run in reverse order when a step fails.
How make Saga steps idempotent?
Use an idempotency key (e.g. SagaId + step or client key) and store processed keys (DB or cache). If the key was already processed, skip or return success. Apply to both forward and compensation handlers.
What if compensation fails?
Retry (with backoff); send to dead-letter after max retries; alert and manual intervention. Design compensation to be retryable and safe (e.g. release that was reserved—idempotent).
Saga vs 2PC (two-phase commit)?
2PC uses a distributed lock (prepare → commit/abort); blocks and is brittle across services. Saga uses local transactions and compensation; no lock, eventual consistency. Prefer Saga in microservices.
Tools for Saga in .NET?
MassTransit (saga state machine), NServiceBus (saga), Azure Durable Functions (orchestration). Or build your own with a state store and message bus (Azure Service Bus, RabbitMQ).
Related Guides & Resources
Explore the matching guide, related services, and more articles.