Waqas Ahmad — Software Architect & Technical Consultant - Available USA, Europe, Global

Waqas Ahmad — Software Architect & Technical Consultant

Specializing in

Distributed Systems

.NET ArchitectureCloud-Native ArchitectureAzure Cloud EngineeringAPI ArchitectureMicroservices ArchitectureEvent-Driven ArchitectureDatabase Design & Optimization

👋 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.

Experienced across engineering ecosystems shaped by Microsoft, the Cloud Native Computing Foundation, and the Apache Software Foundation.

Available for remote consulting (USA, Europe, Global) — flexible across EST, PST, GMT & CET.

services
Article

Saga Pattern: Orchestrator vs Choreography in Microservices

Saga pattern: orchestrator vs choreography, when to use which, and .NET examples.

services
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.

If you are new: start with Topics covered and Saga at a glance.

For a deeper overview of this topic, explore the full Microservices Architecture guide.

Decision Context

  • 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:

  1. Performs a local transaction (e.g. reserve inventory in the Inventory DB).
  2. 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:

  1. Client starts the Saga (e.g. POST place order).
  2. Orchestrator creates Saga state (SagaId, OrderId, Status = Running, CompletedStepIndex = -1) and saves it.
  3. Orchestrator sends ReserveInventoryCommand to Inventory service; waits for success (or message ack).
  4. On success, updates state (CompletedStepIndex = 0), sends ChargePaymentCommand to Payment service.
  5. On success, updates state (CompletedStepIndex = 1), sends CreateShipmentCommand to Shipping service.
  6. On success, updates state (Status = Completed). Saga done.
  7. 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.

Full code: Contracts

// OrderSaga.Contracts/Commands.cs
namespace OrderSaga.Contracts;

public record ReserveInventoryCommand(Guid SagaId, string IdempotencyKey, Guid OrderId, List<OrderLine> Lines);
public record ReleaseInventoryCommand(Guid SagaId, string IdempotencyKey, Guid OrderId);

public record ChargePaymentCommand(Guid SagaId, string IdempotencyKey, Guid OrderId, decimal Amount);
public record RefundPaymentCommand(Guid SagaId, string IdempotencyKey, Guid OrderId);

public record CreateShipmentCommand(Guid SagaId, string IdempotencyKey, Guid OrderId, string Address);
public record CancelShipmentCommand(Guid SagaId, string IdempotencyKey, Guid OrderId);

public record OrderLine(Guid ProductId, int Quantity);

public record OrderSagaRequest(Guid SagaId, string IdempotencyKey, Guid OrderId, List<OrderLine> Lines, decimal Amount, string ShippingAddress);

Full code: Saga state and orchestrator

// OrderSaga.Orchestrator/OrderSagaState.cs
namespace OrderSaga.Orchestrator;

public class OrderSagaState
{
    public Guid SagaId { get; set; }
    public Guid OrderId { get; set; }
    public SagaStatus Status { get; set; }
    public int CompletedStepIndex { get; set; } = -1;
}

public enum SagaStatus { Running, Completed, Compensating, Failed }

// OrderSaga.Orchestrator/SagaOrchestrator.cs
using OrderSaga.Contracts;

namespace OrderSaga.Orchestrator;

public class SagaOrchestrator
{
    private readonly ISagaStateStore _stateStore;
    private readonly ICommandSender _sender;

    public SagaOrchestrator(ISagaStateStore stateStore, ICommandSender sender)
    {
        _stateStore = stateStore;
        _sender = sender;
    }

    public async Task RunOrderSagaAsync(OrderSagaRequest request, CancellationToken ct)
    {
        var state = new OrderSagaState
        {
            SagaId = request.SagaId,
            OrderId = request.OrderId,
            Status = SagaStatus.Running,
            CompletedStepIndex = -1
        };
        await _stateStore.SaveAsync(state, ct).ConfigureAwait(false);

        try
        {
            await ExecuteStepAsync(state, 0, new ReserveInventoryCommand(request.SagaId, request.IdempotencyKey, request.OrderId, request.Lines), ct).ConfigureAwait(false);
            await ExecuteStepAsync(state, 1, new ChargePaymentCommand(request.SagaId, request.IdempotencyKey, request.OrderId, request.Amount), ct).ConfigureAwait(false);
            await ExecuteStepAsync(state, 2, new CreateShipmentCommand(request.SagaId, request.IdempotencyKey, request.OrderId, request.ShippingAddress), ct).ConfigureAwait(false);
            state.Status = SagaStatus.Completed;
        }
        catch (Exception)
        {
            state.Status = SagaStatus.Compensating;
            if (state.CompletedStepIndex >= 2) await _sender.SendAsync(new CancelShipmentCommand(state.SagaId, "cancel-" + state.SagaId, state.OrderId), ct).ConfigureAwait(false);
            if (state.CompletedStepIndex >= 1) await _sender.SendAsync(new RefundPaymentCommand(state.SagaId, "refund-" + state.SagaId, state.OrderId), ct).ConfigureAwait(false);
            if (state.CompletedStepIndex >= 0) await _sender.SendAsync(new ReleaseInventoryCommand(state.SagaId, "release-" + state.SagaId, state.OrderId), ct).ConfigureAwait(false);
            state.Status = SagaStatus.Failed;
        }

        await _stateStore.SaveAsync(state, ct).ConfigureAwait(false);
    }

    private async Task ExecuteStepAsync(OrderSagaState state, int stepIndex, object command, CancellationToken ct)
    {
        await _sender.SendAndWaitAsync(command, ct).ConfigureAwait(false);
        state.CompletedStepIndex = stepIndex;
        await _stateStore.SaveAsync(state, ct).ConfigureAwait(false);
    }
}

public interface ISagaStateStore
{
    Task SaveAsync(OrderSagaState state, CancellationToken ct = default);
}

public interface ICommandSender
{
    Task SendAsync(object command, CancellationToken ct = default);
    Task SendAndWaitAsync(object command, CancellationToken ct = default);
}

Full code: Participant handler (Inventory) with idempotency

// Inventory.Service/ReserveInventoryHandler.cs
using OrderSaga.Contracts;

namespace Inventory.Service;

public class ReserveInventoryHandler
{
    private readonly IIdempotencyStore _idempotency;
    private readonly IInventoryRepository _repo;

    public ReserveInventoryHandler(IIdempotencyStore idempotency, IInventoryRepository repo)
    {
        _idempotency = idempotency;
        _repo = repo;
    }

    public async Task HandleAsync(ReserveInventoryCommand cmd, CancellationToken ct)
    {
        if (await _idempotency.AlreadyProcessedAsync(cmd.IdempotencyKey, ct).ConfigureAwait(false))
            return;

        await _repo.ReserveAsync(cmd.OrderId, cmd.Lines, ct).ConfigureAwait(false);
        await _idempotency.MarkProcessedAsync(cmd.IdempotencyKey, ct).ConfigureAwait(false);
    }
}

public class ReleaseInventoryHandler
{
    private readonly IIdempotencyStore _idempotency;
    private readonly IInventoryRepository _repo;

    public ReleaseInventoryHandler(IIdempotencyStore idempotency, IInventoryRepository repo)
    {
        _idempotency = idempotency;
        _repo = repo;
    }

    public async Task HandleAsync(ReleaseInventoryCommand cmd, CancellationToken ct)
    {
        var key = cmd.IdempotencyKey;
        if (await _idempotency.AlreadyProcessedAsync(key, ct).ConfigureAwait(false))
            return;

        await _repo.ReleaseAsync(cmd.OrderId, ct).ConfigureAwait(false);
        await _idempotency.MarkProcessedAsync(key, ct).ConfigureAwait(false);
    }
}

public interface IIdempotencyStore
{
    Task<bool> AlreadyProcessedAsync(string key, CancellationToken ct = default);
    Task MarkProcessedAsync(string key, CancellationToken ct = default);
}

public interface IInventoryRepository
{
    Task ReserveAsync(Guid orderId, List<OrderLine> lines, CancellationToken ct = default);
    Task ReleaseAsync(Guid orderId, CancellationToken ct = default);
}

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:

  1. Order service (or API) publishes OrderCreated (OrderId, Lines, Amount, Address).
  2. Inventory subscribes, reserves inventory, publishes InventoryReserved (OrderId).
  3. Payment subscribes to InventoryReserved, charges payment, publishes PaymentCharged (OrderId).
  4. Shipping subscribes to PaymentCharged, creates shipment, publishes ShipmentCreated (OrderId). Saga complete.
  5. If Payment fails: Payment publishes PaymentFailed (OrderId). Inventory subscribes to PaymentFailed and runs ReleaseInventory. (Shipping never ran, so no compensation.)
  6. 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).

Diagram:

Loading diagram…

Full code: Events (choreography)

// OrderSaga.Contracts/Events.cs (choreography)
namespace OrderSaga.Contracts.Events;

public record OrderCreated(Guid OrderId, List<OrderLine> Lines, decimal Amount, string ShippingAddress);
public record InventoryReserved(Guid OrderId);
public record PaymentCharged(Guid OrderId);
public record ShipmentCreated(Guid OrderId);

public record PaymentFailed(Guid OrderId, string Reason);
public record ShipmentFailed(Guid OrderId, string Reason);

Full code: Choreography handlers (Inventory)

// Inventory.Service/OrderCreatedHandler.cs (choreography)
namespace Inventory.Service;

public class OrderCreatedHandler
{
    private readonly IInventoryRepository _repo;
    private readonly IEventPublisher _publisher;

    public OrderCreatedHandler(IInventoryRepository repo, IEventPublisher publisher)
    {
        _repo = repo;
        _publisher = publisher;
    }

    public async Task HandleAsync(OrderCreated evt, CancellationToken ct)
    {
        try
        {
            await _repo.ReserveAsync(evt.OrderId, evt.Lines, ct).ConfigureAwait(false);
            await _publisher.PublishAsync(new InventoryReserved(evt.OrderId), ct).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            await _publisher.PublishAsync(new PaymentFailed(evt.OrderId, "Inventory failed: " + ex.Message), ct).ConfigureAwait(false);
        }
    }
}

public class PaymentFailedHandler
{
    private readonly IInventoryRepository _repo;

    public PaymentFailedHandler(IInventoryRepository repo) => _repo = repo;

    public async Task HandleAsync(PaymentFailed evt, CancellationToken ct)
        => await _repo.ReleaseAsync(evt.OrderId, ct).ConfigureAwait(false);
}

public interface IEventPublisher
{
    Task PublishAsync(object evt, CancellationToken ct = default);
}

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).


Full working example: Order Saga end to end

Project structure

OrderSaga.sln
  src/OrderSaga.Orchestrator/     SagaOrchestrator.cs, OrderSagaState.cs, ISagaStateStore.cs
  src/OrderSaga.Contracts/         Commands (orchestrator), Events (choreography)
  src/OrderSaga.Api/               Controllers: POST /order/saga -> RunOrderSagaAsync
  src/Inventory.Service/            ReserveInventoryHandler, ReleaseInventoryHandler (orchestrator)
                                   OrderCreatedHandler, PaymentFailedHandler (choreography)
  src/Payment.Service/             ChargePaymentHandler, RefundPaymentHandler
  src/Shipping.Service/            CreateShipmentHandler, CancelShipmentHandler

API: Start Saga (orchestrator)

// OrderSaga.Api/Controllers/OrderSagaController.cs
using Microsoft.AspNetCore.Mvc;
using OrderSaga.Contracts;
using OrderSaga.Orchestrator;

namespace OrderSaga.Api.Controllers;

[ApiController]
[Route("api/order")]
public class OrderSagaController : ControllerBase
{
    private readonly SagaOrchestrator _orchestrator;

    public OrderSagaController(SagaOrchestrator orchestrator) => _orchestrator = orchestrator;

    [HttpPost("saga")]
    public async Task<ActionResult<Guid>> StartOrderSaga([FromBody] StartOrderRequest request, CancellationToken ct)
    {
        var sagaId = Guid.NewGuid();
        var orderId = Guid.NewGuid();
        var req = new OrderSagaRequest(
            sagaId,
            request.IdempotencyKey ?? sagaId.ToString(),
            orderId,
            request.Lines,
            request.Amount,
            request.ShippingAddress
        );
        await _orchestrator.RunOrderSagaAsync(req, ct).ConfigureAwait(false);
        return AcceptedAtAction(nameof(GetStatus), new { sagaId }, new { sagaId, orderId });
    }

    [HttpGet("saga/{sagaId:guid}")]
    public async Task<ActionResult<SagaStatus>> GetStatus(Guid sagaId, CancellationToken ct)
    {
        var state = await _orchestrator.GetStateAsync(sagaId, ct).ConfigureAwait(false);
        return state == null ? NotFound() : Ok(state.Status);
    }
}

public record StartOrderRequest(string? IdempotencyKey, List<OrderLine> Lines, decimal Amount, string ShippingAddress);

Program.cs (orchestrator + state store + command sender)

// OrderSaga.Api/Program.cs
builder.Services.AddSingleton<ISagaStateStore, InMemorySagaStateStore>();
builder.Services.AddSingleton<ICommandSender, ServiceBusCommandSender>();
builder.Services.AddSingleton<SagaOrchestrator>();
builder.Services.AddControllers();

Best practices and pitfalls

Do:

  • 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.

You can also explore more patterns in the Microservices Architecture resource page.

Key Takeaways

  • 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.


services
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 transaction undoes 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).

services
Related Guides & Resources

services
Related services