Skip to content

Sagas: Orchestration vs Choreography

Key Points

  • A saga is a long-running, multi-step business transaction across services. Each step has a compensating action (rollback equivalent).
  • Orchestration: a central coordinator drives the saga (state machine). Easier to reason; coordinator becomes the bottleneck.
  • Choreography: services react to events; no central coordinator. Decentralized; harder to debug.
  • Use orchestration when steps follow a strict workflow with conditional logic. Use choreography when steps are independent and the system is event-driven.
  • Tools in .NET: MassTransit Saga, Wolverine sagas, Durable Functions, Temporal.io. Pick one — don't hand-roll.

Concepts (deep dive)

Why sagas?

PlaceOrder workflow:
1. Reserve inventory
2. Charge payment
3. Schedule shipping
4. Send confirmation

Each step touches a different service. If step 3 fails, you must undo 1 and 2 (refund payment, release inventory). No distributed transaction. Saga = the explicit "how to coordinate + compensate" pattern.

Orchestration

                 ┌──────────────┐
                 │ Orchestrator │
                 │  (state mach)│
                 └──────┬───────┘
                        │ commands
        ┌───────────────┼───────────────┐
        ▼               ▼               ▼
 ┌────────────┐  ┌────────────┐  ┌────────────┐
 │ Inventory  │  │  Payment   │  │  Shipping  │
 └────────────┘  └────────────┘  └────────────┘

The orchestrator sends commands and reacts to results. It owns the state.

// MassTransit saga
public class OrderSagaState : SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; } = "";
    public Guid OrderId { get; set; }
}

public class OrderSagaMachine : MassTransitStateMachine<OrderSagaState>
{
    public State Reserving { get; private set; } = null!;
    public State Charging { get; private set; } = null!;
    public State Shipping { get; private set; } = null!;
    public State Done { get; private set; } = null!;

    public Event<OrderPlaced> OrderPlaced { get; private set; } = null!;
    public Event<InventoryReserved> InventoryReserved { get; private set; } = null!;
    public Event<PaymentCharged> PaymentCharged { get; private set; } = null!;

    public OrderSagaMachine()
    {
        InstanceState(x => x.CurrentState);

        Initially(
            When(OrderPlaced)
                .Then(c => c.Saga.OrderId = c.Message.OrderId)
                .Publish(c => new ReserveInventory(c.Saga.OrderId))
                .TransitionTo(Reserving));

        During(Reserving,
            When(InventoryReserved)
                .Publish(c => new ChargePayment(c.Saga.OrderId))
                .TransitionTo(Charging),
            When(InventoryFailed)
                .Publish(c => new CancelOrder(c.Saga.OrderId))
                .Finalize());

        During(Charging,
            When(PaymentCharged)
                .Publish(c => new ScheduleShipping(c.Saga.OrderId))
                .TransitionTo(Shipping),
            When(PaymentFailed)
                .Publish(c => new ReleaseInventory(c.Saga.OrderId))   // compensate
                .Finalize());

        // ... etc
    }
}

State persisted (EF, Redis, MongoDB). Saga is durable; survives crashes.

Choreography

[Order]→OrderPlaced→[Inventory]→InventoryReserved→[Payment]→PaymentCharged→[Shipping]
                                       └→ on failure: InventoryFailed→[Order] cancels

Each service publishes events; others react. No central state machine; the choreography emerges from the wiring.

public class InventoryHandler
{
    public async Task Handle(OrderPlaced e)
    {
        try
        {
            await Reserve(e.OrderId, e.Lines);
            await _bus.Publish(new InventoryReserved(e.OrderId));
        }
        catch
        {
            await _bus.Publish(new InventoryFailed(e.OrderId));
        }
    }
}

public class PaymentHandler
{
    public async Task Handle(InventoryReserved e)
    {
        try
        {
            await Charge(e.OrderId);
            await _bus.Publish(new PaymentCharged(e.OrderId));
        }
        catch
        {
            await _bus.Publish(new PaymentFailed(e.OrderId));
        }
    }
}

Compensations

A saga step that fails triggers compensations for previously-completed steps. Compensations are forward actions (publish a refund command, release inventory) — not raw rollbacks.

Compensations must be idempotent — they may run multiple times.

Trade-offs

Aspect Orchestration Choreography
Visibility Central state machine Distributed; harder
Coupling Coordinator knows all Services know contracts
Complexity Coordinator complexity Wiring complexity
Cycles Easy to express Easy to introduce accidentally
Add new step Update coordinator Update event flow
Failure debugging Easy (one state machine) Hard (event sleuth)

When orchestration

  • Strict workflow.
  • Conditional logic (if X, do Y).
  • Need rollback ordering.
  • Auditing the workflow as one unit.

When choreography

  • Pure event-driven.
  • Loosely-coupled services.
  • Steps are mostly independent.
  • New consumers added without changing producers.

Long-running

Sagas can run for hours or days (waiting for human approval, scheduled actions). State must be persisted; correlation IDs link events to saga instances.

Timeout / scheduled

// MassTransit: scheduled message
.Schedule(c => new TimedOut(c.Saga.OrderId), TimeSpan.FromHours(1))

Saga waits 1 hour; if no progress, transitions to TimedOut state and compensates.

Comparing to workflow engines

For very complex workflows, dedicated engines (Temporal.io, Durable Functions) shine. They provide:

  • Versioning of workflow code.
  • Time-travel debugging.
  • Built-in retries, schedules.
  • Visual workflow inspection.

Sagas in MassTransit are great for medium complexity. For real workflow systems, see Workflow Engines.

Saga state storage

.AddSagaStateMachine<OrderSagaMachine, OrderSagaState>()
    .EntityFrameworkRepository(r =>
    {
        r.ConcurrencyMode = ConcurrencyMode.Optimistic;
        r.ExistingDbContext<AppDb>();
    });

EF, MongoDB, Redis, Cosmos DB are common choices. Optimistic concurrency to handle parallel events.

Idempotency in sagas

Each event handler may fire multiple times (at-least-once delivery). Each transition must be idempotent — typically the saga state's "current state" check provides this.

Test sagas

var harness = new InMemoryTestHarness();
var sagaHarness = harness.StateMachineSaga<OrderSagaState, OrderSagaMachine>(new OrderSagaMachine());

await harness.Start();
await harness.Bus.Publish(new OrderPlaced(orderId));

Assert.True(sagaHarness.Consumed.Select<OrderPlaced>().Any());
var saga = sagaHarness.Created.Contains(orderId);
Assert.Equal("Reserving", saga.CurrentState);

MassTransit's test harness simulates the bus. No broker needed.


Code: correct vs wrong

❌ Wrong: distributed transaction across services

using var tx = new TransactionScope();
inventoryService.Reserve(...);
paymentService.Charge(...);
tx.Complete();

XA across services + DB doesn't work in modern microservices.

✅ Correct: saga with compensation

(See OrderSagaMachine above)

❌ Wrong: orchestration with synchronous calls

inventoryClient.Reserve(...);
paymentClient.Charge(...);
shippingClient.Schedule(...);

Coupled; no retry; first failure cascades.

✅ Correct: orchestrator publishes commands; reacts to events

Event-driven; resilient; auditable.


Design patterns for this topic

Pattern 1 — "Saga state machine for workflows"

  • Intent: explicit state + transitions.

Pattern 2 — "Compensations as forward actions"

  • Intent: idempotent, retriable rollbacks.

Pattern 3 — "Scheduled timeouts in saga"

  • Intent: progress guarantees.

Pattern 4 — "Choreography for loosely-coupled events"

  • Intent: add consumers without changing producers.

Pattern 5 — "Workflow engine over hand-rolled saga"

  • Intent: for very complex flows.

Pros & cons / trade-offs

Approach Pros Cons
Orchestration Visible; auditable Coordinator complexity
Choreography Decoupled; scalable Hard to debug
Workflow engine Powerful; versioned Operational dep
Hand-rolled Full control Re-inventing

When to use / when to avoid

  • Use orchestration for strict workflows.
  • Use choreography for event-driven loosely-coupled.
  • Use workflow engines for complex/long-running.
  • Avoid distributed transactions (XA).

Interview Q&A

Q1. What's a saga? Long-running, multi-step distributed transaction with compensating actions for rollback.

Q2. Orchestration vs choreography? Orchestration: central coordinator. Choreography: services react to events; no center.

Q3. Why "compensating action" instead of rollback? You can't truly roll back across services. Compensations are forward actions that undo the effect (refund the charge, release inventory).

Q4. Saga state storage? EF/Mongo/Redis/Cosmos. Optimistic concurrency for parallel events.

Q5. When use workflow engine over saga? Complex multi-day workflows; need versioning, time-travel debugging, visual inspection.

Q6. Failure in choreography — how diagnose? Distributed tracing critical. Without it, debugging is painful.

Q7. Sagas + idempotency? Saga events arrive at-least-once. Compensations and transitions must be idempotent.

Q8. Scheduled timeout in saga? MassTransit .Schedule(...) — saga gets a timeout event after duration; can compensate or escalate.

Q9. Test sagas? InMemoryTestHarness simulates bus. No broker.

Q10. Why not just synchronous service calls? First failure cascades; no retry; tight coupling. Sagas isolate failure and provide explicit compensation.

Q11. Cycles in choreography — risk? Easy to create accidentally. Service A → event → B → event → A → event → B... Test for cycles.

Q12. Order of events vs ordering of saga state? Events arrive out of order. Saga state must handle out-of-order — typically by ignoring events not valid for current state.


Gotchas / common mistakes

  • ⚠️ Non-idempotent compensations.
  • ⚠️ Distributed transactions instead of saga.
  • ⚠️ Choreography without distributed tracing — debug nightmare.
  • ⚠️ Synchronous orchestrator — defeats async benefits.
  • ⚠️ Saga state without optimistic concurrency — race on parallel events.

Further reading