Saga Pattern: Distributed Transactions
Saga Pattern: Distributed Transactions
The Saga Pattern is a failure-management pattern for distributed systems. In a microservices architecture, you cannot use traditional ACID transactions across services. A Saga provides eventual consistency by breaking a large transaction into a series of small, local transactions.
๐๏ธ The Problem
Imagine an E-commerce System. When a user places an order:
- Order Service creates an order.
- Payment Service processes payment.
- Inventory Service reserves stock.
If the payment succeeds but the inventory is out of stock, you need to โundoโ the payment. Since there is no global transaction, you use a Saga to manage this.
๐ Two Types of Sagas
1. Choreography (Event-Based)
Each service produces and listens to events from other services.
- Pros: Simple for small workflows, no central point of failure.
- Cons: Hard to track the state of a complex workflow.
2. Orchestration (Command-Based)
A central โOrchestratorโ (Saga State Machine) tells each service what to do and when.
- Pros: Centralized logic, easy to debug complex workflows.
- Cons: The orchestrator can become a โGod Object.โ
๐ The .NET Implementation (Orchestration with MassTransit)
In .NET, MassTransit is the gold standard for implementing Sagas.
1. The Saga State (Data)
public class OrderState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; } // Unique ID for the Saga
public string CurrentState { get; set; } // e.g., "Submitted", "Paid", "Failed"
public Guid OrderId { get; set; }
}2. The State Machine (Logic)
public class OrderSaga : MassTransitStateMachine<OrderState>
{
public State Submitted { get; private set; }
public State Paid { get; private set; }
public Event<OrderSubmitted> OrderSubmitted { get; private set; }
public Event<PaymentConfirmed> PaymentConfirmed { get; private set; }
public OrderSaga()
{
InstanceState(x => x.CurrentState);
Initially(
When(OrderSubmitted)
.Then(context => context.Saga.OrderId = context.Message.OrderId)
.TransitionTo(Submitted)
.Publish(context => new ProcessPayment { OrderId = context.Saga.OrderId })
);
During(Submitted,
When(PaymentConfirmed)
.TransitionTo(Paid)
.Publish(context => new ReserveInventory { OrderId = context.Saga.OrderId })
);
}
}๐ Compensating Transactions
If a step fails, the Saga must trigger โCompensating Transactionsโ to undo the previous steps (e.g., RefundPayment if ReserveInventory fails).
๐ก Why use Saga?
- Consistency: Ensures distributed systems reach a consistent state.
- Resilience: Handles partial failures gracefully.
- Scalability: Services remain independent and asynchronous.