SAGA Design Pattern in Microservices
Hi Readers,
Today I am going to explain very important topic SAGA(SAGA means very long story) Design pattern in springboot microservice.
Problem Statement:
Microservices come with their own advantages and disadvantages.
One such disadvantage is managing distributed transactions.
Let’s say your transaction spans 4 different microservices.
How do you ensure that your transaction either commits successfully with all the 4 tasks succeeding or fails successfully
if any of the tasks is not completed ( the completed ones are rolled back)?
Spring Boot provides the annotation @Transactional to manage transactions.
But this works only within a single method and within a single project.
Solution:
There is a design pattern which solves the issue with distributed transactions in microservices.
It was originally proposed by the computer scientist Hector Gracia Molina and Kenneth Salem.
As suggested , they created this for Long Lived Transactions (LLT) and not for microservices but it serves well for microservices.
A long lived transaction is a transaction which takes a longer time , may be minutes , hours or even days. You can’t lock a database until all the tasks in such transaction completes for it will severely affect the performance of the application. Hence they came up with the design pattern SAGA (probably named SAGA because they created it for dealing with long transactions — SAGA means a very long story).
It can be implemented in two ways:
1.Choreography
2. Orchestration
Now Lets see complete implementation of this.
The SAGA pattern is a design pattern for ensuring data consistency across microservices without using distributed transactions. It consists of a series of local transactions, each updating the database and publishing an event or sending a command to trigger the next local transaction in the saga. There are two main types of SAGA implementations: Choreography and Orchestration.
1. Choreography-Based SAGA
In this approach, each service involved in the transaction communicates through events. There is no central orchestrator; instead, each service listens to events and produces its own events.
Example: Order Processing System
- Order Service: Starts the order and publishes an event.
- Payment Service: Listens to the order event, processes the payment, and publishes a payment event.
- Inventory Service: Listens to the payment event, reserves items, and publishes an inventory event.
- Shipping Service: Listens to the inventory event and prepares the shipment.
Code Example
OrderService.java
@Service
public class OrderService {
@Autowired
private KafkaTemplate<String, OrderEvent> kafkaTemplate;
public void createOrder(OrderRequest orderRequest) {
// Logic to create the order
kafkaTemplate.send("order-topic", new OrderEvent(orderRequest));
}
public void cancelOrder(OrderRequest orderRequest) {
// Logic to cancel the order
kafkaTemplate.send("order-topic", new CancelOrderEvent(orderRequest));
}
}
PaymentService.java
@Service
public class PaymentService {
@KafkaListener(topics = "order-topic")
public void handleOrderEvent(OrderEvent orderEvent) {
// Logic to process payment
kafkaTemplate.send("payment-topic", new PaymentEvent(orderEvent.getOrderId(), paymentSuccessful));
}
}
InventoryService.java
@Service
public class InventoryService {
@KafkaListener(topics = "payment-topic")
public void handlePaymentEvent(PaymentEvent paymentEvent) {
// Logic to reserve items
kafkaTemplate.send("inventory-topic", new InventoryEvent(paymentEvent.getOrderId(), itemsReserved));
}
}
ShippingService.java
@Service
public class ShippingService {
@KafkaListener(topics = "inventory-topic")
public void handleInventoryEvent(InventoryEvent inventoryEvent) {
// Logic to prepare shipment
System.out.println("Shipment prepared for order: " + inventoryEvent.getOrderId());
}
}
2. Orchestration-Based SAGA
In this approach, a central orchestrator service manages the saga. It coordinates the saga’s steps by sending commands to other services and waiting for responses.
Example: Order Processing System
- Saga Orchestrator: Manages the order process and coordinates between services.
- Order Service: Creates the order.
- Payment Service: Processes the payment.
- Inventory Service: Reserves items.
- Shipping Service: Prepares the shipment.
Code Example
SagaOrchestrator.java
@Component
public class SagaOrchestrator {
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
@Autowired
private ShippingService shippingService;
public void startSaga(OrderRequest orderRequest) throws Exception {
try {
orderService.createOrder(orderRequest);
paymentService.processPayment(orderRequest);
inventoryService.reserveItems(orderRequest);
shippingService.prepareShipment(orderRequest);
} catch (Exception e) {
compensate(orderRequest);
throw new Exception("Saga failed, rolling back");
}
}
private void compensate(OrderRequest orderRequest) {
shippingService.cancelShipment(orderRequest);
inventoryService.releaseItems(orderRequest);
paymentService.refundPayment(orderRequest);
orderService.cancelOrder(orderRequest);
}
}
OrderService.java
@Service
public class OrderService {
public void createOrder(OrderRequest orderRequest) {
// Logic to create the order
System.out.println("Order created: " + orderRequest.getOrderId());
}
public void cancelOrder(OrderRequest orderRequest) {
// Logic to cancel the order
System.out.println("Order canceled: " + orderRequest.getOrderId());
}
}
PaymentService.java
@Service
public class PaymentService {
public void processPayment(OrderRequest orderRequest) {
// Logic to process payment
System.out.println("Payment processed for order: " + orderRequest.getOrderId());
}
public void refundPayment(OrderRequest orderRequest) {
// Logic to refund payment
System.out.println("Payment refunded for order: " + orderRequest.getOrderId());
}
}
InventoryService.java
@Service
public class InventoryService {
public void reserveItems(OrderRequest orderRequest) {
// Logic to reserve items
System.out.println("Items reserved for order: " + orderRequest.getOrderId());
}
public void releaseItems(OrderRequest orderRequest) {
// Logic to release items
System.out.println("Items released for order: " + orderRequest.getOrderId());
}
}
ShippingService.java
@Service
public class ShippingService {
public void prepareShipment(OrderRequest orderRequest) {
// Logic to prepare shipment
System.out.println("Shipment prepared for order: " + orderRequest.getOrderId());
}
public void cancelShipment(OrderRequest orderRequest) {
// Logic to cancel shipment
System.out.println("Shipment canceled for order: " + orderRequest.getOrderId());
}
}
Conclusion
- Events/Choreography: Each service independently handles its part of the transaction, responding to events from other services. This approach works well for simple workflows and highly decoupled services.
- Command/Orchestration: A central orchestrator service coordinates the entire transaction, issuing commands to each service. This approach is better for complex workflows requiring centralized control.
Choosing the right approach depends on the specific requirements and complexity of your microservices architecture.
Keep Learning…
Happy Learning :)
Neeraj Srivastava