DEV Community

Cover image for Implementing the Saga Pattern with Spring Boot and ActiveMQ in Microservice
Jacky
Jacky

Posted on

Implementing the Saga Pattern with Spring Boot and ActiveMQ in Microservice

Microservice have revolutionized the way we build and deploy software applications, offering scalability, flexibility, and independence for different parts of the system. However, managing distributed transactions in a microservice architecture can be challenging. This is where the Saga pattern comes into play. In this article, we will explore the Saga pattern and provide examples of implementing it using Spring Boot and ActiveMQ.

Understanding the Saga Pattern

The Saga pattern is a design pattern used to manage distributed transactions within a microservice architecture. It addresses the complexities and challenges of maintaining data consistency across multiple microservice, all while avoiding the pitfalls of traditional distributed transactions.

In the Saga pattern, a global transaction is broken down into a sequence of smaller, local transactions, each associated with a specific microservice. If one step fails, a compensating action is triggered to undo previous steps, ensuring data consistency.

Saga Pattern in Action

To better understand how the Saga pattern works, let's consider a booking system. Create three separate Spring Boot projects for:

  1. Booking Service: Responsible for handling hotel room booking.
  2. Payment Service: Manages the payment process.
  3. Saga Orchestrator: Orchestrates the SAGA pattern for the booking and payment processes.
  4. Shared Database (PostgreSQL): Stores data related to bookings and payments.
  5. Message Broker (ActiveMQ): Handles asynchronous communication between microservices.

If a failure occurs at any step, the Saga pattern ensures that the appropriate compensating action is triggered. For instance, if the payment processing fails, a compensating action may involve canceling the order or marking it as unpaid.

Setting up Microservice

For this example, we will create two Spring Boot microservices: Order Service and Payment Service. Each service will have its own PostgreSQL database.

1. Booking Service Code:

@Service
public class BookingService {
    @Autowired
    private JmsTemplate jmsTemplate;

    @Autowired
    private BookingRepository bookingRepository;

    @Transactional
    public void makeBooking(Booking booking) {
        bookingRepository.save(booking);
        jmsTemplate.convertAndSend("bookingQueue", booking);
    }

    @Transactional
    public void confirmBooking(Long bookingId) {
        Booking booking = bookingRepository.findById(bookingId).orElse(null);
        if (booking != null) {
            booking.setConfirmed(true);
            bookingRepository.save(booking);
        }
    }

    @Transactional
    public void cancelBooking(Long bookingId) {
        Booking booking = bookingRepository.findById(bookingId).orElse(null);
        if (booking != null) {
            bookingRepository.delete(booking);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Payment Service Code

@Service
public class PaymentService {
    @Autowired
    private JmsTemplate jmsTemplate;

    @Autowired
    private PaymentRepository paymentRepository;

    @Transactional
    public void processPayment(Booking booking) {
        Payment payment = new Payment();
        payment.setBookingId(booking.getId());
        payment.setAmount(calculatePaymentAmount(booking));
        paymentRepository.save(payment);
        jmsTemplate.convertAndSend("paymentQueue", payment);
    }

    @Transactional
    public void confirmPayment(Long bookingId) {
        Payment payment = paymentRepository.findByBookingId(bookingId);
        if (payment != null) {
            payment.setPaid(true);
            paymentRepository.save(payment);
        }
    }

    @Transactional
    public void cancelPayment(Long bookingId) {
        Payment payment = paymentRepository.findByBookingId(bookingId);
        if (payment != null) {
            paymentRepository.delete(payment);
        }
    }

    private double calculatePaymentAmount(Booking booking) {
        // Implement your payment calculation logic here
        return booking.getRoomPrice() * booking.getNumNights();
    }
}
Enter fullscreen mode Exit fullscreen mode

3. SagaOrchestrator Service:

@Service
public class SagaOrchestrator {
    @Autowired
    private BookingService bookingService;

    @Autowired
    private PaymentService paymentService;

    @JmsListener(destination = "bookingQueue")
    public void handleBooking(Booking booking) {

    }

    @JmsListener(destination = "paymentQueue")
    public void handlePayment(Payment payment) {
        try {
                        // step 2: confirm payment is success or failed. If it's failed
                        // It's failure, throw exception and rollback.  
            paymentService.confirmPayment(payment.getBookingId());

                        // Step 3: Mark status is comfirmed in booking.
                        bookingService.confirmBooking(payment.getBookingId())
        } catch (Exception e) {
            // Handle exceptions and initiate compensation
                        bookingService.cancelBooking(booking.getId());
            paymentService.cancelPayment(payment.getBookingId());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Workflow step by step:

Step 1: User request new process booking with function: makeBooking

Step 2: After process booking is created, user should be make a payment to continue process.

Step 3: Send payment event to handlePayment function in SagaOrchestrator

  • If payment is success, confirmed
  • If payment is failure, execute rollback and cancel booking and payment.

Please note that this is a simplified example, and a real-world implementation may involve more robust error handling, retries, and monitoring for better resilience and fault tolerance. Additionally, you might want to consider using a framework like Spring Cloud State Machine or external tools for SAGA pattern management in more complex scenarios.

Benefits and Considerations

The Saga pattern offers numerous benefits in a microservice architecture:

  • Decentralization: Each microservice is responsible for its part of the global transaction, reducing the need for a centralized coordinator.
  • Scalability: As each step of the saga is a local transaction, it's easier to scale individual microservice independently.
  • Resilience: In case of failures, the pattern allows for easy recovery by executing compensating actions, ensuring data consistency.
  • Performance: Avoiding distributed transactions can improve system performance.

However, it's essential to consider the complexity of managing sagas and implementing compensating actions. Furthermore, eventual consistency is a trade-off, and it may not be suitable for all applications.

In summary, the Saga pattern is a valuable tool for managing distributed transactions in microservice architecture. It allows you to maintain data consistency while breaking down complex distributed transactions into smaller, more manageable steps. By adopting ActiveMQ as your messaging system, you can enable seamless communication between microservices, ensuring the robustness and reliability of your microservices-based applications.

Top comments (0)