The Distributed Transaction Problem
In microservices, a single business operation often spans multiple services. Traditional ACID transactions don't work across service boundaries. The Saga pattern provides a solution.
What is a Saga?
A saga is a sequence of local transactions. Each local transaction updates the database and publishes events or messages. If a step fails, compensating transactions undo the preceding steps.
Example: Order Processing Saga
1. Create Order (Orders Service)
2. Reserve Inventory (Inventory Service)
3. Process Payment (Payment Service)
4. Ship Order (Shipping Service)
If step 3 fails:
- Compensate: Release Inventory
- Compensate: Cancel OrderImplementation Approaches
Choreography
Each service listens for events and decides when to act:
// Orders Service
OrderCreated โ publishes event
// Inventory Service
Listens for OrderCreated โ reserves stock โ publishes InventoryReserved
// Payment Service
Listens for InventoryReserved โ processes payment โ publishes PaymentProcessed
// Shipping Service
Listens for PaymentProcessed โ ships orderPros:
- Loose coupling
- Simple for small sagas
Cons:
- Hard to track saga state
- Cyclic dependencies risk
Orchestration
A central orchestrator coordinates the saga:
public class OrderSagaOrchestrator {
public async Task Execute(CreateOrderCommand cmd) {
var saga = new OrderSaga(cmd.OrderId);
try {
await saga.Step("CreateOrder",
() => _orders.Create(cmd));
await saga.Step("ReserveInventory",
() => _inventory.Reserve(cmd.Items),
() => _inventory.Release(cmd.Items)); // Compensate
await saga.Step("ProcessPayment",
() => _payment.Process(cmd.PaymentInfo),
() => _payment.Refund(cmd.PaymentInfo));
await saga.Complete();
} catch {
await saga.Compensate();
}
}
}Pros:
- Clear saga flow
- Easy to add steps
- Centralized error handling
Cons:
- Orchestrator can become complex
- Single point of coordination
Handling Failures
Compensating Transactions
Design compensations that are semantically opposite:
- CreateOrder โ CancelOrder
- ReserveInventory โ ReleaseInventory
- ChargePayment โ RefundPayment
Idempotency
Both forward and compensating transactions must be idempotent to handle retries safely.
Saga State Management
public class SagaState {
public Guid SagaId { get; set; }
public string CurrentStep { get; set; }
public SagaStatus Status { get; set; }
public List<CompletedStep> CompletedSteps { get; set; }
}Best Practices
- Keep sagas short (fewer steps = less complexity)
- Design for failure from the start
- Use correlation IDs for tracing
- Implement timeouts and deadlines
- Test compensation paths thoroughly
Conclusion
The Saga pattern enables complex distributed operations while maintaining data consistency. Choose choreography for simple flows and orchestration for complex multi-step processes.