Saga Pattern and Distributed Transactions
Coordinate complex workflows across services with the Saga pattern.
A Simple Analogy
Saga is like a choreographed dance. Each dancer (service) knows their moves. If someone falls, the dance has a recovery move to restore balance.
Why Sagas?
- Distributed transactions: Coordinate across services
- Eventual consistency: Accept temporary inconsistency
- Resilience: Compensating transactions for rollback
- Loose coupling: Services don't know each other
- Auditing: Complete history of state changes
Orchestration Pattern
public class OrderSaga : IOrderSaga
{
private readonly IOrderService _orderService;
private readonly IPaymentService _paymentService;
private readonly IInventoryService _inventoryService;
private readonly IShippingService _shippingService;
public async Task<Order> ExecuteAsync(Order order)
{
try
{
// Step 1: Create order
var createdOrder = await _orderService.CreateAsync(order);
// Step 2: Process payment
var payment = await _paymentService.AuthorizeAsync(order.Total);
if (!payment.Success)
throw new PaymentFailedException();
// Step 3: Reserve inventory
var reserved = await _inventoryService.ReserveAsync(order.Items);
if (!reserved)
throw new InventoryUnavailableException();
// Step 4: Create shipment
var shipment = await _shippingService.CreateShipmentAsync(order);
return createdOrder;
}
catch (Exception ex)
{
// Compensate: undo all changes
await RollbackAsync(order);
throw;
}
}
private async Task RollbackAsync(Order order)
{
// Undo in reverse order
await _shippingService.CancelShipmentAsync(order.Id);
await _inventoryService.ReleaseReservationAsync(order.Id);
await _paymentService.RefundAsync(order.Id);
await _orderService.CancelAsync(order.Id);
}
}
Choreography Pattern
// OrderService publishes events
public class OrderService
{
private readonly IPublishEndpoint _publishEndpoint;
public async Task<Order> CreateAsync(Order order)
{
await SaveAsync(order);
// Publish event - other services react
await _publishEndpoint.Publish(new OrderCreated
{
OrderId = order.Id,
CustomerId = order.CustomerId,
Total = order.Total,
Items = order.Items
});
return order;
}
}
// PaymentService listens to OrderCreated
public class PaymentConsumer : IConsumer<OrderCreated>
{
public async Task Consume(ConsumeContext<OrderCreated> context)
{
var result = await _paymentService.AuthorizeAsync(context.Message.Total);
if (result.Success)
{
// Publish event for next service
await context.Publish(new PaymentAuthorized
{
OrderId = context.Message.OrderId,
TransactionId = result.TransactionId
});
}
else
{
// Publish failure event for compensation
await context.Publish(new OrderCancelled
{
OrderId = context.Message.OrderId,
Reason = "Payment failed"
});
}
}
}
// InventoryService listens to PaymentAuthorized
public class InventoryConsumer : IConsumer<PaymentAuthorized>
{
public async Task Consume(ConsumeContext<PaymentAuthorized> context)
{
var reserved = await _inventoryService.ReserveAsync(context.Message.OrderId);
if (reserved)
{
await context.Publish(new InventoryReserved
{
OrderId = context.Message.OrderId
});
}
else
{
// Compensation chain
await context.Publish(new OrderCancelled
{
OrderId = context.Message.OrderId,
Reason = "Inventory unavailable"
});
}
}
}
// All services handle cancellation
public class CancellationConsumer : IConsumer<OrderCancelled>
{
public async Task Consume(ConsumeContext<OrderCancelled> context)
{
// Release resources
await _paymentService.RefundAsync(context.Message.OrderId);
await _inventoryService.ReleaseAsync(context.Message.OrderId);
}
}
State Machine Saga
public class OrderFulfillmentState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public string OrderId { get; set; }
public decimal Total { get; set; }
public string TransactionId { get; set; }
}
public class OrderFulfillmentStateMachine : StateMachine<OrderFulfillmentState>
{
public State OrderSubmitted { get; set; }
public State PaymentProcessing { get; set; }
public State PaymentApproved { get; set; }
public State InventoryReserved { get; set; }
public State Shipped { get; set; }
public State OrderCancelled { get; set; }
public Event<OrderSubmitted> SubmitOrder { get; set; }
public Event<PaymentApproved> PaymentOk { get; set; }
public Event<PaymentFailed> PaymentFailed { get; set; }
public Event<InventoryReserved> InventoryOk { get; set; }
public OrderFulfillmentStateMachine()
{
InstanceState(x => x.CurrentState);
Initially(
When(SubmitOrder)
.Then(context => context.Instance.OrderId = context.Data.OrderId)
.TransitionTo(PaymentProcessing)
.Publish(context => new ProcessPayment { OrderId = context.Data.OrderId })
);
During(PaymentProcessing,
When(PaymentOk)
.Then(context => context.Instance.TransactionId = context.Data.TransactionId)
.TransitionTo(PaymentApproved)
.Publish(context => new ReserveInventory { OrderId = context.Instance.OrderId }),
When(PaymentFailed)
.TransitionTo(OrderCancelled)
);
During(PaymentApproved,
When(InventoryOk)
.TransitionTo(InventoryReserved)
.Publish(context => new ShipOrder { OrderId = context.Instance.OrderId })
);
}
}
Best Practices
- Idempotent operations: Handle duplicate events
- Timeout handling: What if service doesn't respond?
- Compensating transactions: Design undo operations
- Observability: Track saga progress
- Testing: Test happy and failure paths
Related Concepts
- Eventual consistency
- Event sourcing
- CQRS pattern
- Distributed transactions
Summary
Sagas coordinate complex workflows across services. Use orchestration for simple flows, choreography for loosely-coupled systems, and state machines for complex logic.
Related Articles
Polly Resilience Patterns
Implement resilience with retry, circuit breaker, and bulkhead patterns.
Read More devopsDocker Compose Advanced Guide
Master advanced Docker Compose features for production.
Read More devopsDocker Compose Orchestration
Orchestrate multi-container applications with Docker Compose.
Read More