What is the Saga pattern for distributed 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; }
}