Isaac.

patterns

Saga Pattern and Distributed Transactions

Coordinate complex workflows across services with the Saga pattern.

By Emem IsaacAugust 12, 20244 min read
#saga#distributed transactions#eventual consistency#orchestration
Share:

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.

Share:

Written by Emem Isaac

Expert Software Engineer with 15+ years of experience building scalable enterprise applications. Specialized in ASP.NET Core, Azure, Docker, and modern web development. Passionate about sharing knowledge and helping developers grow.

Ready to Build Something Amazing?

Let's discuss your project and explore how my expertise can help you achieve your goals. Free consultation available.

💼 Trusted by 50+ companies worldwide | ⚡ Average response time: 24 hours