Saga Patterns: Distributed Transaction Management
Sagas manage transactions that span multiple services. Instead of ACID transactions, sagas use a sequence of local transactions with compensating actions for rollback.
TL;DR
| Pattern | Coordination | Best For |
|---|---|---|
| Choreography | Events, no central coordinator | Simple flows, few services |
| Orchestration | Central coordinator | Complex flows, many services |
The Problem
Distributed transactions (2PC) don't scale:
❌ Two-Phase Commit Problems:
• Locks resources across services
• Coordinator is single point of failure
• Doesn't work across different databases
• Performance bottleneck
Saga Solution
If any step fails, compensating actions run:
Pattern 1: Choreography
Services communicate through events, no central coordinator:
Implementation
// Order Service - starts saga
public class OrderService
{
public async Task CreateOrder(CreateOrderCommand cmd)
{
var order = Order.Create(cmd.CustomerId, cmd.Items);
await _orderRepository.Save(order);
// Publish event to start saga
await _eventBus.Publish(new OrderCreatedEvent(
order.Id,
order.Items.Select(i => new OrderItemDto(i.ProductId, i.Quantity)).ToList()
));
}
}
// Inventory Service - reacts to OrderCreated
public class InventoryEventHandler : IEventHandler<OrderCreatedEvent>
{
public async Task Handle(OrderCreatedEvent @event)
{
try
{
foreach (var item in @event.Items)
{
await _inventoryService.Reserve(item.ProductId, item.Quantity, @event.OrderId);
}
await _eventBus.Publish(new StockReservedEvent(@event.OrderId));
}
catch (InsufficientStockException)
{
await _eventBus.Publish(new StockReservationFailedEvent(@event.OrderId));
}
}
}
// Payment Service - reacts to StockReserved
public class PaymentEventHandler : IEventHandler<StockReservedEvent>
{
public async Task Handle(StockReservedEvent @event)
{
var order = await _orderClient.GetOrder(@event.OrderId);
try
{
await _paymentService.ProcessPayment(order.CustomerId, order.Total);
await _eventBus.Publish(new PaymentProcessedEvent(@event.OrderId));
}
catch (PaymentFailedException)
{
await _eventBus.Publish(new PaymentFailedEvent(@event.OrderId));
}
}
}
// Inventory Service - compensates on PaymentFailed
public class PaymentFailedHandler : IEventHandler<PaymentFailedEvent>
{
public async Task Handle(PaymentFailedEvent @event)
{
// Compensating action
await _inventoryService.ReleaseReservation(@event.OrderId);
}
}
// Order Service - compensates on failures
public class OrderCompensationHandler :
IEventHandler<StockReservationFailedEvent>,
IEventHandler<PaymentFailedEvent>
{
public async Task Handle(StockReservationFailedEvent @event)
{
await _orderService.CancelOrder(@event.OrderId, "Insufficient stock");
}
public async Task Handle(PaymentFailedEvent @event)
{
await _orderService.CancelOrder(@event.OrderId, "Payment failed");
}
}
Choreography Trade-offs
| Pros | Cons |
|---|---|
| Loose coupling | Hard to understand flow |
| Simple services | No central view of saga state |
| No single point of failure | Cyclic dependencies possible |
| Easy to add services | Difficult to test end-to-end |
Pattern 2: Orchestration
Central coordinator manages the saga:
Implementation
// Saga orchestrator
public class PlaceOrderSaga : Saga<PlaceOrderSagaData>,
IAmStartedBy<PlaceOrderCommand>,
IHandleMessages<OrderCreatedReply>,
IHandleMessages<StockReservedReply>,
IHandleMessages<StockReservationFailedReply>,
IHandleMessages<PaymentProcessedReply>,
IHandleMessages<PaymentFailedReply>
{
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<PlaceOrderSagaData> mapper)
{
mapper.MapSaga(s => s.OrderId)
.ToMessage<PlaceOrderCommand>(m => m.OrderId)
.ToMessage<OrderCreatedReply>(m => m.OrderId)
.ToMessage<StockReservedReply>(m => m.OrderId)
// ... other mappings
}
public async Task Handle(PlaceOrderCommand message, IMessageHandlerContext context)
{
Data.OrderId = message.OrderId;
Data.CustomerId = message.CustomerId;
Data.Items = message.Items;
Data.CurrentStep = SagaStep.CreatingOrder;
// Step 1: Create order
await context.Send(new CreateOrderCommand(message.OrderId, message.CustomerId, message.Items));
}
public async Task Handle(OrderCreatedReply message, IMessageHandlerContext context)
{
Data.CurrentStep = SagaStep.ReservingStock;
// Step 2: Reserve stock
await context.Send(new ReserveStockCommand(Data.OrderId, Data.Items));
}
public async Task Handle(StockReservedReply message, IMessageHandlerContext context)
{
Data.StockReserved = true;
Data.CurrentStep = SagaStep.ProcessingPayment;
// Step 3: Process payment
await context.Send(new ProcessPaymentCommand(Data.OrderId, Data.CustomerId, message.Total));
}
public async Task Handle(StockReservationFailedReply message, IMessageHandlerContext context)
{
// Compensation: Cancel order
await context.Send(new CancelOrderCommand(Data.OrderId, "Insufficient stock"));
MarkAsComplete();
}
public async Task Handle(PaymentProcessedReply message, IMessageHandlerContext context)
{
Data.PaymentProcessed = true;
Data.CurrentStep = SagaStep.CreatingShipment;
// Step 4: Create shipment
await context.Send(new CreateShipmentCommand(Data.OrderId));
}
public async Task Handle(PaymentFailedReply message, IMessageHandlerContext context)
{
// Compensation: Release stock, then cancel order
Data.CurrentStep = SagaStep.Compensating;
await context.Send(new ReleaseStockCommand(Data.OrderId));
await context.Send(new CancelOrderCommand(Data.OrderId, "Payment failed"));
MarkAsComplete();
}
}
public class PlaceOrderSagaData : ContainSagaData
{
public Guid OrderId { get; set; }
public Guid CustomerId { get; set; }
public List<OrderItem> Items { get; set; }
public SagaStep CurrentStep { get; set; }
public bool StockReserved { get; set; }
public bool PaymentProcessed { get; set; }
}
Orchestration Trade-offs
| Pros | Cons |
|---|---|
| Clear flow visibility | Orchestrator can be bottleneck |
| Easier to understand | Single point of failure |
| Centralized error handling | Coupling to orchestrator |
| Easier testing | More complex orchestrator code |
Choosing Between Patterns
┌─────────────────────────────────────────────────────────┐
│ CHOREOGRAPHY vs ORCHESTRATION │
├─────────────────────────────────────────────────────────┤
│ │
│ Use CHOREOGRAPHY when: │
│ • Few services (2-4) │
│ • Simple, linear flow │
│ • Services are truly independent │
│ • Team prefers decentralized approach │
│ │
│ Use ORCHESTRATION when: │
│ • Many services (5+) │
│ • Complex flow with branches │
│ • Need visibility into saga state │
│ • Complex compensation logic │
│ • Regulatory/audit requirements │
│ │
└─────────────────────────────────────────────────────────┘
Compensation Design
Principles
- Semantic undo - Not technical rollback
- Idempotent - Can be retried safely
- Order matters - Reverse order of forward steps
// Forward: Reserve → Charge → Ship
// Compensation: Cancel Shipment → Refund → Release
public class OrderSagaCompensations
{
// Compensation must be idempotent
public async Task ReleaseStock(Guid orderId)
{
var reservation = await _reservationRepo.GetByOrderId(orderId);
if (reservation == null || reservation.Status == ReservationStatus.Released)
return; // Already compensated
reservation.Release();
await _reservationRepo.Save(reservation);
}
// Semantic compensation (refund, not "un-charge")
public async Task RefundPayment(Guid orderId)
{
var payment = await _paymentRepo.GetByOrderId(orderId);
if (payment == null || payment.Status == PaymentStatus.Refunded)
return; // Already compensated
await _paymentGateway.Refund(payment.TransactionId, payment.Amount);
payment.MarkRefunded();
await _paymentRepo.Save(payment);
}
}
Quick Reference
┌─────────────────────────────────────────────────────────┐
│ SAGA PATTERNS QUICK REFERENCE │
├─────────────────────────────────────────────────────────┤
│ │
│ CHOREOGRAPHY │
│ • Services publish/subscribe events │
│ • No central coordinator │
│ • Best for simple flows │
│ │
│ ORCHESTRATION │
│ • Central saga coordinator │
│ • Explicit flow control │
│ • Best for complex flows │
│ │
│ COMPENSATION RULES │
│ • Semantic undo (not rollback) │
│ • Must be idempotent │
│ • Reverse order of forward steps │
│ │
└─────────────────────────────────────────────────────────┘
Next Steps
- CQRS Deep Dive - Read/write separation
- Event Sourcing - Saga with events