When building a modern microservices architecture, one of the most significant challenges developers face is maintaining database consistency across services. In a traditional monolithic application, transaction management is simple: you wrap database operations in a single database transaction, and the database engine handles the Atomicity, Consistency, Isolation, and Durability (ACID) properties. However, when you split your application into multiple microservices—each with its own database—maintaining a single database transaction becomes impossible. This is where distributed transaction management patterns like the Saga Pattern come into play.
The Limitations of Two-Phase Commit (2PC)
Before exploring the Saga Pattern, it is important to understand why the traditional approach, Two-Phase Commit (2PC), fails in modern, cloud-scale microservice architectures:
- Blocking Protocol: 2PC is a blocking protocol. During the prepare phase, the coordinator locks the database rows in all participating databases. These locks are held until the coordinator decides to commit or rollback. This severely limits throughput and scalability.
- Single Point of Failure: The coordinator is a single point of failure. If the coordinator crashes during the transaction, the database locks remain open, stalling the system.
- Lack of Microservices Compatibility: Many modern NoSQL databases and distributed message brokers do not support 2PC or XA transactions. 2PC violates the autonomy of microservices, forcing them to coordinate synchronously.
What is the Saga Pattern?
The Saga Pattern solves the distributed transaction problem by breaking a global transaction into a series of local transactions. Each local transaction updates the database within a single service and publishes an event or message.
- If all local transactions succeed, the Saga is complete.
- If any local transaction fails (e.g., due to insufficient funds or stock), the Saga executes compensating transactions in reverse order to undo the changes made by the previous steps.
Unlike traditional transactions, Sagas offer eventual consistency rather than immediate ACID consistency.
Choreography vs. Orchestration Sagas
There are two primary ways to coordinate a Saga: Choreography (decentralized) and Orchestration (centralized).
#### 1. Choreography-Based Saga
In a choreography-based Saga, there is no central controller. Instead, services interact by publishing and subscribing to events.
- Workflow:
1. The Order Service creates an order in a PENDING state and publishes an OrderCreated event.
2. The Payment Service listens for the OrderCreated event, processes the payment, and publishes a PaymentSuccessful event.
3. The Inventory Service listens for PaymentSuccessful, reserves the items, and publishes an InventoryReserved event.
4. The Order Service listens for InventoryReserved and updates the order status to COMPLETED.
- Pros: Simple to implement for simple workflows; highly decentralized; no single point of failure.
- Cons: Hard to understand as the number of services grows; risk of cyclic dependencies; difficult to debug.
#### 2. Orchestration-Based Saga
In an orchestration-based Saga, a central component called the Saga Orchestrator coordinates the workflow. It decides which local transaction to execute next based on the state machine.
- Workflow:
1. The Order Service receives a request and creates a pending order, then delegating control to the OrderSagaOrchestrator.
2. The Orchestrator sends a command to the Payment Service to process payment.
3. The Payment Service replies with success.
4. The Orchestrator sends a command to the Inventory Service to reserve stock.
5. The Inventory Service replies with success.
6. The Orchestrator sends a command to the Order Service to approve the order.
- Pros: Centralized visibility; easy to manage complex workflows; avoids cyclic dependencies.
- Cons: Introduces a single point of failure (the orchestrator); additional complexity in managing the orchestrator state.
Implementing Compensating Transactions for Rollbacks
Compensating transactions are the "undo" actions in a Saga. If the Inventory Service finds that items are out of stock, it sends a failure response. The Saga must then rollback.
- If using Choreography, the Inventory Service publishes an
InventoryOutOfStockevent. The Payment Service listens to this and triggers a refund, while the Order Service updates the order toCANCELLED. - If using Orchestration, the Orchestrator catches the inventory failure and issues explicit commands to refund payment and cancel the order.
Code Outline: Simple Choreography Saga Event Model in Spring Boot
Below is a conceptual implementation of how you can structure a choreography-based Saga event listener in Spring Boot using Spring Kafka.
First, let's look at the Order Service publishing the initial event:
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate;
@Transactional
public Order createOrder(OrderRequest request) {
Order order = Order.builder()
.customerId(request.getCustomerId())
.amount(request.getAmount())
.status(OrderStatus.PENDING)
.build();
orderRepository.save(order);
// Publish event to start the Saga
kafkaTemplate.send("order-created-topic", new OrderCreatedEvent(order.getId(), order.getCustomerId(), order.getAmount()));
return order;
}
}
Next, the Payment Service listens to the event and processes payment:
@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentEventListener {
private final PaymentService paymentService;
private final KafkaTemplate<String, Object> kafkaTemplate;
@KafkaListener(topics = "order-created-topic", groupId = "payment-group")
public void handleOrderCreated(OrderCreatedEvent event) {
log.info("Received OrderCreatedEvent for order: {}", event.getOrderId());
try {
paymentService.processPayment(event.getOrderId(), event.getCustomerId(), event.getAmount());
// Publish success event
kafkaTemplate.send("payment-success-topic", new PaymentSuccessEvent(event.getOrderId()));
} catch (InsufficientFundsException e) {
log.error("Payment failed for order: {}", event.getOrderId());
// Publish rollback/failure event
kafkaTemplate.send("payment-failed-topic", new PaymentFailedEvent(event.getOrderId(), e.getMessage()));
}
}
}
Finally, the Order Service listens for payment outcomes to resolve the Saga:
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderSagaListener {
private final OrderRepository orderRepository;
@KafkaListener(topics = "payment-success-topic", groupId = "order-group")
public void handlePaymentSuccess(PaymentSuccessEvent event) {
orderRepository.findById(event.getOrderId()).ifPresent(order -> {
order.setStatus(OrderStatus.COMPLETED);
orderRepository.save(order);
log.info("Order {} successfully completed.", event.getOrderId());
});
}
@KafkaListener(topics = "payment-failed-topic", groupId = "order-group")
public void handlePaymentFailed(PaymentFailedEvent event) {
orderRepository.findById(event.getOrderId()).ifPresent(order -> {
order.setStatus(OrderStatus.FAILED);
order.setFailureReason(event.getReason());
orderRepository.save(order);
log.warn("Order {} failed and marked as cancelled.", event.getOrderId());
});
}
}
Interview Strategy for Indian Software Engineers
When interviewing at top companies like Oracle, Walmart, or Flipkart in Bengaluru or Hyderabad, distributed transaction questions are highly popular.
- Never suggest 2PC first: If asked how to handle transactions in microservices, immediately point out that 2PC is not scalable and introduce Sagas instead.
- Be clear on consistency: Explain that Sagas trade immediate consistency for eventual consistency.
- Know the trade-offs: Contrast Choreography and Orchestration. Choose Orchestration if the business flow has more than 3-4 steps, as Choreography becomes unmanageable.