Isaac.

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

  1. Idempotent operations: Handle duplicate events
  2. Timeout handling: What if service doesn't respond?
  3. Compensating transactions: Design undo operations
  4. Observability: Track saga progress
  5. 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.