Saga Pattern — Choreography vs Orchestration

Distributed Transaction Management for Microservices


Introduction

In microservices architectures, traditional ACID transactions aren't possible across service boundaries. When an order placement requires inventory reservation, payment processing, and shipping notification—each running in different services—you need a pattern to manage distributed transactions. The Saga pattern provides a mechanism to coordinate multiple services while maintaining data consistency without distributed locks.

This comprehensive guide covers:

  • Saga fundamentals — Understanding the pattern
  • Choreography approach — Event-driven coordination
  • Orchestration approach — Centralized controller
  • Compensating transactions — Handling failures
  • Implementation patterns — Code examples
  • Choosing the right approach — Decision framework

Understanding the Saga Pattern

The Distributed Transaction Problem

┌─────────────────────────────────────────────────────────────────────┐
│               DISTRIBUTED TRANSACTION CHALLENGE                     │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   Traditional Transaction (Single Database):                        │
│   ──────────────────────────────────────────                        │
│   BEGIN TRANSACTION                                                 │
│     UPDATE inventory SET stock = stock - 1 WHERE product = 'xyz'    │
│     INSERT INTO orders (product, amount) VALUES ('xyz', 100)        │
│     UPDATE accounts SET balance = balance - 100 WHERE user = 'abc'. │
│   COMMIT                                                            │
│   ✓ Atomic: All or nothing                                          │
│   ✓ Consistent: Data always valid                                   │
│   ✓ Isolated: Concurrent changes don't interfere                    │
│   ✓ Durable: Committed data persists                                │
│                                                                     │
│   Microservices Transaction (Sagas):                                │
│   ─────────────────────────────────────                             │
│   Service A: Reserve Inventory  ──┐                                 │
│   Service B: Process Payment    ──┼── Coordination required!        │
│   Service C: Create Shipment    ──┘                                 │
│   ✗ No distributed transaction support                              │
│   ✗ Network can fail at any point                                   │
│   ✓ Solution: Saga with compensating actions                        │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

How Saga Works

┌─────────────────────────────────────────────────────────────────────┐
│                      SAGA PATTERN FLOW                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   SAGA: Sequence of Local Transactions + Compensating Actions       │
│                                                                     │
│   ┌──────────────────────────────────────────────────────────────┐  │
│   │                    ORDER SERVICE                             │  │
│   │                                                              │  │
│   │   1. Create Order (pending)                                  │  │
│   │       │                                                      │  │
│   │       ▼                                                      │  │
│   │   2. ────── EVENT ──────> Inventory Service                  │  │
│   │       │                        │                             │  │
│   │       ▼                        ▼                             │  │
│   │   3. <───── SUCCESS ────── Reserve Inventory ✓               │  │
│   │       │                        │                             │  │
│   │       ▼                        ▼                             │  │
│   │   4. ────── EVENT ──────> Payment Service                    │  │
│   │       │                        │                             │  │
│   │       ▼                        ▼                             │  │
│   │   5. <───── SUCCESS ────── Process Payment ✓                 │  │
│   │       │                        │                             │  │
│   │       ▼                        ▼                             │  │
│   │   6. ────── EVENT ──────> Shipping Service                   │  │
│   │       │                        │                             │  │
│   │       ▼                        ▼                             │  │
│   │   7. <───── SUCCESS ────── Create Shipment ✓                 │  │
│   │       │                                                      │  │
│   │       ▼                                                      │  │
│   │   8. Order Completed ✓                                       │  │
│   │                                                              │  │
│   └──────────────────────────────────────────────────────────────┘  │
│                                                                     │
│   If any step fails: Execute compensating actions in reverse        │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Choreography Approach

Event-Driven Coordination

┌─────────────────────────────────────────────────────────────────────┐
│                    CHOREOGRAPHY SAGAS                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   Event Bus (Service Bus / Event Grid)                              │
│   ─────────────────────────────────────                             │
│         │                   │                   │                   │
│         ▼                   ▼                   ▼                   │
│   ┌────────────┐     ┌────────────┐     ┌────────────┐              │
│   │ Inventory  │     │  Payment   │     │ Shipping   │              │
│   │  Service   │     │  Service   │     │  Service   │              │
│   │            │     │            │     │            │              │
│   │ OrderCreated│───▶│ Reserved   │───▶ │ Scheduled  │              │
│   │  (publish) │     │ (listen)   │     │ (listen)   │              │
│   │            │     │            │     │            │              │
│   │ Compensate │◀────│ Compensate │◀────│ Complete   │              │
│   │  (listen)  │     │  (listen)  │     │ (publish)  │              │
│   └────────────┘     └────────────┘     └────────────┘              │
│                                                                     │
│   Pros: Loose coupling, no central orchestrator                     │
│   Cons: Hard to track, complex debugging                            │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Choreography Implementation

// Order Service - Publishes events
public class OrderService
{
    private readonly ServiceBusClient _serviceBus;

    [FunctionName("CreateOrder")]
    public async Task<IActionResult> CreateOrder([HttpTrigger] HttpRequest req)
    {
        var order = await req.ReadFromJsonAsync<Order>();
        
        var orderCreated = new OrderCreatedEvent
        {
            OrderId = order.Id,
            CustomerId = order.CustomerId,
            Items = order.Items,
            Total = order.Total
        };
        
        await PublishEventAsync("order-created", orderCreated);
        
        return new Accepted();
    }

    [FunctionName("HandleCompensateOrder")]
    public async Task HandleCompensateOrder(
        [ServiceBusTrigger("order-failed", "compensation")] ServiceBusMessage message)
    {
        var failedEvent = message.Body.ToObjectFromJson<OrderFailedEvent>();
        
        await UpdateOrderStatusAsync(failedEvent.OrderId, "Compensated");
        await PublishEventAsync("order-compensated", new OrderCompensatedEvent
        {
            OrderId = failedEvent.OrderId,
            Reason = failedEvent.Reason
        });
    }

    private async Task PublishEventAsync(string topic, object eventData)
    {
        var sender = _serviceBus.CreateSender("integration-events");
        var message = new ServiceBusMessage(JsonSerializer.Serialize(eventData))
        {
            ContentType = "application/json",
            Subject = topic
        };
        await sender.SendMessageAsync(message);
    }
}

// Inventory Service - Listens and reacts
public class InventoryService
{
    [FunctionName("ReserveInventory")]
    public async Task Run(
        [ServiceBusTrigger("integration-events", "order-created")] ServiceBusMessage message)
    {
        var orderEvent = message.Body.ToObjectFromJson<OrderCreatedEvent>();
        
        try
        {
            // Reserve inventory
            await ReserveInventoryItemsAsync(orderEvent.Items);
            
            // Publish success event
            await PublishEventAsync("inventory-reserved", new InventoryReservedEvent
            {
                OrderId = orderEvent.OrderId,
                Items = orderEvent.Items
            });
        }
        catch (InsufficientStockException ex)
        {
            // Publish failure - triggers compensation
            await PublishEventAsync("inventory-failed", new InventoryFailedEvent
            {
                OrderId = orderEvent.OrderId,
                Reason = ex.Message
            });
        }
    }

    [FunctionName("CompensateInventory")]
    public async Task CompensateInventory(
        [ServiceBusTrigger("integration-events", "compensation")] ServiceBusMessage message)
    {
        var compensationEvent = message.Body.ToObjectFromJson<OrderFailedEvent>();
        
        // Release reserved inventory
        await ReleaseInventoryAsync(compensationEvent.OrderId);
    }
}

Orchestration Approach

Centralized Controller

┌─────────────────────────────────────────────────────────────────────┐
│                  ORCHESTRATION SAGAS                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │              SAGA ORCHESTRATOR (Controller)                 │   │
│   │                                                             │   │
│   │   Orchestrator                                              │   │
│   │   ┌───────────────────────────────────────────────────┐     │   │
│   │   │  OrderSaga                                        │     │   │
│   │   │                                                   │     │   │
│   │   │  Step 1: Reserve Inventory ─────────────────────┐ │     │   │
│   │   │  Step 2: Process Payment ───────────────────────┼─┤     │   │
│   │   │  Step 3: Create Shipment ───────────────────────┼─┤     │   │
│   │   │  Step 4: Update Order Status ───────────────────┘ │     │   │
│   │   │                                                   │     │   │
│   │   │  Compensation: Undo on failure                    │     │   │
│   │   │  - Release Inventory                              │     │   │
│   │   │  - Refund Payment                                 │     │   │
│   │   │  - Cancel Shipment                                │     │   │
│   │   └───────────────────────────────────────────────────┘     │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                │                                    │
│        ┌───────────────────────┼───────────────────┐                │
│        ▼                       ▼                   ▼                │
│   ┌────────────┐     ┌────────────┐     ┌────────────┐              │
│   │ Inventory  │     │  Payment   │     │ Shipping   │              │
│   │  Service   │     │  Service   │     │  Service   │              │
│   └────────────┘     └────────────┘     └────────────┘              │
│                                                                     │
│   Pros: Centralized logic, easier debugging, better visibility      │
│   Cons: More coupling, single point of failure                      │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Orchestration Implementation

// Saga Orchestrator
public class OrderSagaOrchestrator
{
    private readonly ServiceBusClient _serviceBus;

    public async Task<SagaResult> ExecuteOrderSaga(Order order)
    {
        var sagaState = new OrderSagaState
        {
            OrderId = order.Id,
            Order = order,
            CurrentStep = 0,
            CompletedSteps = new List<int>()
        };

        try
        {
            // Step 1: Reserve Inventory
            sagaState.CurrentStep = 1;
            var inventoryResult = await CallInventoryServiceAsync(order);
            if (!inventoryResult.Success)
                throw new SagaException("Inventory reservation failed", inventoryResult.Error);
            sagaState.InventoryReservationId = inventoryResult.ReservationId;
            sagaState.CompletedSteps.Add(1);

            // Step 2: Process Payment
            sagaState.CurrentStep = 2;
            var paymentResult = await CallPaymentServiceAsync(order);
            if (!paymentResult.Success)
                throw new SagaException("Payment processing failed", paymentResult.Error);
            sagaState.PaymentTransactionId = paymentResult.TransactionId;
            sagaState.CompletedSteps.Add(2);

            // Step 3: Create Shipment
            sagaState.CurrentStep = 3;
            var shipmentResult = await CallShippingServiceAsync(order);
            if (!shipmentResult.Success)
                throw new SagaException("Shipment creation failed", shipmentResult.Error);
            sagaState.ShipmentId = shipmentResult.ShipmentId;
            sagaState.CompletedSteps.Add(3);

            // Step 4: Update Order Status
            await UpdateOrderStatusAsync(order.Id, "Completed");

            return new SagaResult { Success = true, OrderId = order.Id };
        }
        catch (SagaException ex)
        {
            // Compensate completed steps in reverse order
            await CompensateSagaAsync(sagaState, ex.Message);
            return new SagaResult { Success = false, Error = ex.Message };
        }
    }

    private async Task CompensateSagaAsync(OrderSagaState state, string reason)
    {
        // Compensate in reverse order of completion
        foreach (var step in state.CompletedSteps.OrderByDescending(s => s))
        {
            try
            {
                switch (step)
                {
                    case 3:
                        await CancelShipmentAsync(state.ShipmentId);
                        break;
                    case 2:
                        await RefundPaymentAsync(state.PaymentTransactionId);
                        break;
                    case 1:
                        await ReleaseInventoryAsync(state.InventoryReservationId);
                        break;
                }
            }
            catch (Exception compEx)
            {
                // Log compensation failure - may need manual intervention
                await LogCompensationFailureAsync(state.OrderId, step, compEx);
            }
        }

        await UpdateOrderStatusAsync(state.OrderId, "Failed", reason);
    }
}

Compensating Transactions

Designing Compensations

// Each service implements compensation logic
public class CompensationActions
{
    // Inventory compensation
    public async Task CompensateInventoryReservation(string reservationId)
    {
        var inventoryService = GetInventoryService();
        
        // Release the reserved items
        await inventoryService.ReleaseReservationAsync(reservationId);
        
        // Record compensation in compensation log
        await LogCompensationAsync("InventoryReleased", new 
        {
            ReservationId = reservationId,
            CompensatedAt = DateTime.UtcNow
        });
    }

    // Payment compensation
    public async Task CompensatePayment(string transactionId, decimal amount)
    {
        var paymentService = GetPaymentService();
        
        // Initiate refund
        var refundResult = await paymentService.RefundAsync(transactionId, amount);
        
        if (!refundResult.Success)
        {
            // Log for manual intervention
            await LogManualInterventionRequiredAsync("PaymentRefund", new
            {
                TransactionId = transactionId,
                Amount = amount,
                Reason = refundResult.Error
            });
        }
    }

    // Shipping compensation
    public async Task CompensateShipment(string shipmentId)
    {
        var shippingService = GetShippingService();
        
        // Cancel shipment if not yet shipped
        await shippingService.CancelShipmentAsync(shipmentId);
    }
}

Idempotency in Sagas

public class IdempotentSagaStep
{
    public async Task<SagaStepResult> ExecuteStep(
        string stepId, 
        Func<Task<StepResult>> action)
    {
        // Check if step already completed
        var existingResult = await GetStepResultAsync(stepId);
        if (existingResult != null)
        {
            return existingResult;
        }

        // Execute the action
        var result = await action();
        
        // Store result with idempotency key
        await StoreStepResultAsync(stepId, result);
        
        return result;
    }

    // Idempotency keys based on business operations
    // Inventory: orderId + productId
    // Payment: orderId + amount + timestamp
    // Shipping: orderId + address
}

Decision Framework

Choreography vs Orchestration

┌─────────────────────────────────────────────────────────────────────┐
│                    SAGA PATTERN COMPARISON                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   Use CHOREOGRAPHY when:                                            │
│   ─────────────────────────                                         │
│   ✓ Team autonomy is important                                      │
│   ✓ Services are loosely coupled                                    │
│   ✓ Simple workflows (2-3 steps)                                    │
│   ✓ Teams prefer event-driven architecture                          │
│   ✓ Independent service evolution needed                            │
│                                                                     │
│   Use ORCHESTRATION when:                                           │
│   ──────────────────────────                                        │
│   ✓ Complex workflows (many steps)                                  │
│   ✓ Centralized monitoring required                                 │
│   ✓ Rollback logic is complex                                       │
│   ✓ Transaction boundaries are well-defined                         │
│   ✓ Single team owns the业务流程                                     │
│                                                                     │
│   Hybrid Approach:                                                  │
│   ─────────────────                                                 │
│   ✓ Use orchestration for core business transactions                │
│   ✓ Use choreography for integration events                         │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Best Practices

Implementation Checklist

PracticeDescription
Define compensation actionsPlan for failures upfront
Use idempotency keysPrevent duplicate operations
Implement timeout handlingPrevent indefinite waits
Log saga stateEnable debugging and recovery
Design for partial failureHandle partial completion
Monitor saga executionTrack completion and failures

Saga State Persistence

public class SagaState
{
    public string SagaId { get; set; }
    public string SagaType { get; set; }
    public SagaStatus Status { get; set; }
    public int CurrentStep { get; set; }
    public List<int> CompletedSteps { get; set; }
    public Dictionary<string, object> StepResults { get; set; }
    public DateTime StartedAt { get; set; }
    public DateTime? CompletedAt { get; set; }
    public string FailureReason { get; set; }
}

public enum SagaStatus
{
    Started,
    InProgress,
    Completed,
    Compensating,
    Compensated,
    Failed
}

Related Topics


Azure Integration Hub - Architect Level Enterprise Integration Patterns