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
| Practice | Description |
|---|---|
| Define compensation actions | Plan for failures upfront |
| Use idempotency keys | Prevent duplicate operations |
| Implement timeout handling | Prevent indefinite waits |
| Log saga state | Enable debugging and recovery |
| Design for partial failure | Handle partial completion |
| Monitor saga execution | Track 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
- Event Sourcing Architecture — Event-driven state management
- Outbox Pattern — Reliable event publishing
- Competing Consumers — Scalable message processing
Azure Integration Hub - Architect Level Enterprise Integration Patterns