What is the Saga pattern for distributed transactions?

4 minadvancedmicroservicessagadistributed-transactions

Quick Answer

The Saga pattern maintains data consistency across services without distributed transactions by modeling a business transaction as a sequence of local transactions, each publishing an event that triggers the next; failures trigger compensating transactions that undo prior steps. It comes in two styles: choreography (services react to each other's events) and orchestration (a central coordinator directs the steps). It provides eventual consistency for multi-service workflows.

Detailed Answer

The Saga pattern manages data consistency across microservices in distributed transactions by breaking the transaction into a series of local transactions, each with a compensating transaction to undo changes if something fails.

Two Types:

1. Choreography-based Saga (Event-driven):

// Order Service
public class OrderService
{
    public async Task CreateOrder(CreateOrderRequest request)
    {
        var order = new Order { Status = OrderStatus.Pending };
        await _repository.AddAsync(order);
        
        // Publish event to start saga
        await _eventBus.PublishAsync(new OrderCreatedEvent
        {
            OrderId = order.Id,
            CustomerId = request.CustomerId,
            Items = request.Items,
            Total = request.Total
        });
        
        return order;
    }
    
    // Compensating transaction
    public async Task CancelOrder(OrderCancelledEvent @event)
    {
        await _repository.UpdateStatusAsync(@event.OrderId, OrderStatus.Cancelled);
    }
}

// Inventory Service
public class InventoryEventHandler
{
    public async Task Handle(OrderCreatedEvent @event)
    {
        try
        {
            // Reserve inventory
            await _inventoryService.ReserveItemsAsync(@event.Items);
            
            // Publish success event
            await _eventBus.PublishAsync(new InventoryReservedEvent
            {
                OrderId = @event.OrderId,
                Items = @event.Items
            });
        }
        catch (InsufficientStockException)
        {
            // Publish failure event
            await _eventBus.PublishAsync(new InventoryReservationFailedEvent
            {
                OrderId = @event.OrderId
            });
        }
    }
    
    // Compensating transaction
    public async Task Handle(OrderCancelledEvent @event)
    {
        await _inventoryService.ReleaseReservationAsync(@event.OrderId);
    }
}

// Payment Service
public class PaymentEventHandler
{
    public async Task Handle(InventoryReservedEvent @event)
    {
        try
        {
            await _paymentService.ProcessPaymentAsync(@event.OrderId);
            
            await _eventBus.PublishAsync(new PaymentProcessedEvent
            {
                OrderId = @event.OrderId
            });
        }
        catch (PaymentFailedException)
        {
            // Trigger compensation
            await _eventBus.PublishAsync(new PaymentFailedEvent
            {
                OrderId = @event.OrderId
            });
        }
    }
    
    // Compensating transaction
    public async Task Handle(OrderCancelledEvent @event)
    {
        await _paymentService.RefundAsync(@event.OrderId);
    }
}

2. Orchestration-based Saga (Centralized coordinator):

// Saga Orchestrator
public class OrderSagaOrchestrator
{
    private readonly IOrderService _orderService;
    private readonly IInventoryService _inventoryService;
    private readonly IPaymentService _paymentService;
    
    public async Task ExecuteOrderSaga(CreateOrderRequest request)
    {
        var sagaState = new SagaState();
        
        try
        {
            // Step 1: Create Order
            var order = await _orderService.CreateOrderAsync(request);
            sagaState.OrderId = order.Id;
            sagaState.CompletedSteps.Add(SagaStep.OrderCreated);
            
            // Step 2: Reserve Inventory
            await _inventoryService.ReserveItemsAsync(order.Id, request.Items);
            sagaState.CompletedSteps.Add(SagaStep.InventoryReserved);
            
            // Step 3: Process Payment
            await _paymentService.ProcessPaymentAsync(order.Id, request.Total);
            sagaState.CompletedSteps.Add(SagaStep.PaymentProcessed);
            
            // Step 4: Confirm Order
            await _orderService.ConfirmOrderAsync(order.Id);
            
            return SagaResult.Success(order.Id);
        }
        catch (Exception ex)
        {
            // Compensate in reverse order
            await CompensateAsync(sagaState);
            return SagaResult.Failure(ex.Message);
        }
    }
    
    private async Task CompensateAsync(SagaState state)
    {
        if (state.CompletedSteps.Contains(SagaStep.PaymentProcessed))
        {
            await _paymentService.RefundAsync(state.OrderId);
        }
        
        if (state.CompletedSteps.Contains(SagaStep.InventoryReserved))
        {
            await _inventoryService.ReleaseReservationAsync(state.OrderId);
        }
        
        if (state.CompletedSteps.Contains(SagaStep.OrderCreated))
        {
            await _orderService.CancelOrderAsync(state.OrderId);
        }
    }
}

// Saga State Management
public class SagaState
{
    public int OrderId { get; set; }
    public List CompletedSteps { get; set; } = new();
}

public enum SagaStep
{
    OrderCreated,
    InventoryReserved,
    PaymentProcessed
}

// Using MassTransit for Saga Orchestration
public class OrderStateMachine : MassTransitStateMachine
{
    public OrderStateMachine()
    {
        InstanceState(x => x.CurrentState);
        
        Event(() => OrderCreated);
        Event(() => InventoryReserved);
        Event(() => PaymentProcessed);
        
        Initially(
            When(OrderCreated)
                .Then(context => context.Instance.OrderId = context.Data.OrderId)
                .TransitionTo(AwaitingInventory)
                .Publish(context => new ReserveInventoryCommand(context.Data.OrderId)));
        
        During(AwaitingInventory,
            When(InventoryReserved)
                .TransitionTo(AwaitingPayment)
                .Publish(context => new ProcessPaymentCommand(context.Data.OrderId)));
        
        During(AwaitingPayment,
            When(PaymentProcessed)
                .TransitionTo(Completed)
                .Publish(context => new OrderCompletedEvent(context.Data.OrderId)));
    }
    
    public State AwaitingInventory { get; private set; }
    public State AwaitingPayment { get; private set; }
    public State Completed { get; private set; }
    
    public Event OrderCreated { get; private set; }
    public Event InventoryReserved { get; private set; }
    public Event PaymentProcessed { get; private set; }
}

Related Resources