Saga Pattern
Saga Pattern
It manages distributed transactions across microservices by coordinating a series of local transactions. Each service executes its transaction and triggers the next via events, with compensating actions undoing changes if a failure occurs.
- Ensures consistency in multi-service workflows, such as order processing involving payment, inventory and shipping.
- Provides fault tolerance by rolling back previous steps through compensating transactions when any step fails.
Implementation:
- Choreography-Based Approach (Event-Driven):
- There is no central coordinator. Each service produces and listens to events from other services and decides if an action should be taken.
- If a service fails, it publishes a failure event, and other services can listen to it and perform compensating actions.
- Best for: Simple workflows with few services.
- Pros: Easy to understand; no single point of failure.
- Cons: Can become a “spaghetti” of events as the system grows; hard to debug.
- Orchestration (Command-Based):
- A central “Orchestrator” (Saga Manager) tells the participants what local transactions to execute. It acts as the brain of the operation.
- The orchestrator manages failure recovery and calls the compensating actions if needed.
- Best for: Complex workflows with many steps or circular dependencies.
- Pros: Centralized logic; easier to monitor state.
- Cons: Risk of over-centralizing logic in the orchestrator; additional service to manage.
The “ACID” Problem in Sagas: Sagas are ACD but not I (Isolated). Because local transactions commit immediately, other users might see “dirty data” before the Saga finishes.
Example: A customer sees their bank balance drop (Payment Success) but the order fails a second later (Inventory Shortage). The balance then “magically” goes back up (Refund).
Countermeasures:
- Semantic Lock: Use states like PENDING or LOCKING to prevent other processes from using that data until the Saga completes.
- Pessimistic View: Show the user the “worst-case” scenario until confirmation.
Saga vs. Two-Phase Commit (2PC)
| Feature | Two-Phase Commit (2PC) | Saga Pattern |
|---|---|---|
| Consistency | Strong (Atomic) | Eventual |
| Performance | Low (Locks resources) | High (Non-blocking) |
| Scalability | Hard to scale | Built for microservices |
| Mechanism | Centralized Coordinator/Prepare-Commit | Distributed Events/Commands |
Note: 2PC (Two-Phase Commit) is a protocol used to ensure all participants in a distributed transaction either commit or abort, ensuring consistency. In the first phase, the coordinator asks all participants to agree to commit, and in the second phase, participants either vote to commit or abort the transaction.
Java example with Spring boot and Kafka
The Workflow
- Order Service: Saves order as
PENDING, emitsOrderCreatedEvent. - Payment Service: Listens for
OrderCreatedEvent, charges the user, emitsPaymentProcessedEvent. - Order Service: Listens for
PaymentProcessedEvent, updates status toCOMPLETED.
The Order service acts as both the starter of the saga and the listener for its completion.
@Service
public class OrderService {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@Transactional
public void createOrder(OrderRequest request) {
// 1. Save local transaction
Order order = orderRepository.save(new Order(request, "PENDING"));
// 2. Emit event to start Saga
kafkaTemplate.send("order-events", new OrderCreatedEvent(order.getId(), order.getAmount()));
}
@KafkaListener(topics = "payment-events", groupId = "order-group")
public void handlePaymentEvent(PaymentProcessedEvent event) {
Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
if ("SUCCESS".equals(event.getStatus())) {
order.setStatus("COMPLETED");
} else {
// COMPENSATING ACTION: If payment failed, reject the order
order.setStatus("REJECTED");
}
orderRepository.save(order);
}
}The Payment service doesn’t know about the “Order Logic”, it only knows how to process payments for an incoming ID.
@Service
public class PaymentService {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@KafkaListener(topics = "order-events", groupId = "payment-group")
public void processOrderPayment(OrderCreatedEvent event) {
try {
// Logic to charge credit card
paymentProcessor.charge(event.getAmount());
// Emit success event
kafkaTemplate.send("payment-events", new PaymentProcessedEvent(event.getOrderId(), "SUCCESS"));
} catch (Exception e) {
// Emit failure event (triggers compensation in Order Service)
kafkaTemplate.send("payment-events", new PaymentProcessedEvent(event.getOrderId(), "FAILED"));
}
}
}