Saga Pattern and Distributed Transactions
Coordinate complex workflows across services with the Saga pattern.
By EMEPublished: February 20, 2025
sagadistributed transactionseventual consistencyorchestration
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.