DEV Community

Cover image for Mastering Saga Pattern in Spring Boot: Streamline Complex Microservice Transactions
Aarav Joshi
Aarav Joshi

Posted on

Mastering Saga Pattern in Spring Boot: Streamline Complex Microservice Transactions

Let's talk about using the Saga pattern in Spring Boot to handle complex transactions across microservices. This approach is super useful when you're dealing with distributed systems and need to keep everything in sync.

First off, what's a saga? It's basically a way to manage a series of local transactions that work together to complete a bigger business process. Each step in the saga does its own thing, and if something goes wrong, we have a plan to undo or fix it.

There are two main flavors of sagas: choreography and orchestration. With choreography, each service knows what to do next and triggers the next step. Orchestration, on the other hand, has a central coordinator that tells each service what to do.

Let's start with a choreography-based saga in Spring Boot. Here's a simple example:

@Service
public class OrderService {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    public void createOrder(Order order) {
        // Save order
        saveOrder(order);

        // Publish event
        kafkaTemplate.send("order-created", order.getId());
    }

    @KafkaListener(topics = "payment-processed")
    public void handlePaymentProcessed(String orderId) {
        // Update order status
        updateOrderStatus(orderId, "PAID");
    }
}
Enter fullscreen mode Exit fullscreen mode

In this setup, each service publishes events when it completes its part of the transaction. Other services listen for these events and react accordingly.

Now, let's look at an orchestration-based saga. We'll use Spring State Machine to manage the saga's state:

@Configuration
@EnableStateMachine
public class OrderSagaStateMachineConfig extends StateMachineConfigurerAdapter<OrderState, OrderEvent> {

    @Override
    public void configure(StateMachineStateConfigurer<OrderState, OrderEvent> states) throws Exception {
        states
            .withStates()
            .initial(OrderState.CREATED)
            .state(OrderState.PAYMENT_PENDING)
            .state(OrderState.STOCK_CHECKING)
            .state(OrderState.COMPLETED)
            .state(OrderState.FAILED);
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions) throws Exception {
        transitions
            .withExternal()
                .source(OrderState.CREATED).target(OrderState.PAYMENT_PENDING)
                .event(OrderEvent.INITIATE_PAYMENT)
                .and()
            .withExternal()
                .source(OrderState.PAYMENT_PENDING).target(OrderState.STOCK_CHECKING)
                .event(OrderEvent.PAYMENT_COMPLETED)
                .and()
            .withExternal()
                .source(OrderState.STOCK_CHECKING).target(OrderState.COMPLETED)
                .event(OrderEvent.STOCK_CONFIRMED)
                .and()
            .withExternal()
                .source(OrderState.STOCK_CHECKING).target(OrderState.FAILED)
                .event(OrderEvent.STOCK_UNAVAILABLE);
    }
}
Enter fullscreen mode Exit fullscreen mode

This state machine defines the flow of our saga. Each state represents a step in the process, and events trigger transitions between states.

One of the trickiest parts of implementing sagas is handling failures and compensating actions. If something goes wrong halfway through, we need to undo or compensate for the steps we've already taken. Here's how we might handle a failure in our order saga:

@Service
public class OrderSagaOrchestrator {

    @Autowired
    private StateMachine<OrderState, OrderEvent> stateMachine;

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private InventoryService inventoryService;

    public void processOrder(Order order) {
        stateMachine.start();

        try {
            stateMachine.sendEvent(OrderEvent.INITIATE_PAYMENT);
            paymentService.processPayment(order);

            stateMachine.sendEvent(OrderEvent.PAYMENT_COMPLETED);
            boolean stockAvailable = inventoryService.checkStock(order);

            if (stockAvailable) {
                stateMachine.sendEvent(OrderEvent.STOCK_CONFIRMED);
            } else {
                stateMachine.sendEvent(OrderEvent.STOCK_UNAVAILABLE);
                compensate(order);
            }
        } catch (Exception e) {
            compensate(order);
        }
    }

    private void compensate(Order order) {
        if (stateMachine.getState().getId() == OrderState.STOCK_CHECKING) {
            paymentService.refundPayment(order);
        }
        // Add more compensation logic as needed
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, if we find that stock is unavailable after processing payment, we trigger a compensation action to refund the payment.

Idempotency is another crucial concept when working with sagas. We need to ensure that even if a step is repeated (due to retries or network issues), it doesn't cause problems. Here's how we might implement an idempotent payment service:

@Service
public class IdempotentPaymentService {

    @Autowired
    private PaymentRepository paymentRepository;

    @Transactional
    public void processPayment(String orderId, BigDecimal amount) {
        String idempotencyKey = generateIdempotencyKey(orderId);

        if (paymentRepository.findByIdempotencyKey(idempotencyKey).isPresent()) {
            // Payment already processed, do nothing
            return;
        }

        // Process the payment
        Payment payment = new Payment(orderId, amount, idempotencyKey);
        paymentRepository.save(payment);
    }

    private String generateIdempotencyKey(String orderId) {
        return orderId + "-payment";
    }
}
Enter fullscreen mode Exit fullscreen mode

This service checks if a payment with the same idempotency key has already been processed. If so, it doesn't process the payment again.

When it comes to testing sagas, it can get pretty complex. We need to test not just the happy path, but also various failure scenarios and compensation actions. Here's a simple test case using JUnit and Mockito:

@RunWith(SpringRunner.class)
@SpringBootTest
public class OrderSagaOrchestratorTest {

    @Autowired
    private OrderSagaOrchestrator orchestrator;

    @MockBean
    private PaymentService paymentService;

    @MockBean
    private InventoryService inventoryService;

    @Test
    public void testSuccessfulOrderProcessing() {
        Order order = new Order("123", BigDecimal.valueOf(100));

        when(paymentService.processPayment(order)).thenReturn(true);
        when(inventoryService.checkStock(order)).thenReturn(true);

        orchestrator.processOrder(order);

        verify(paymentService).processPayment(order);
        verify(inventoryService).checkStock(order);
        // Add more verifications as needed
    }

    @Test
    public void testCompensationWhenStockUnavailable() {
        Order order = new Order("123", BigDecimal.valueOf(100));

        when(paymentService.processPayment(order)).thenReturn(true);
        when(inventoryService.checkStock(order)).thenReturn(false);

        orchestrator.processOrder(order);

        verify(paymentService).processPayment(order);
        verify(inventoryService).checkStock(order);
        verify(paymentService).refundPayment(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

These tests check both the successful case and a case where compensation is needed.

Monitoring sagas in production is crucial for maintaining a healthy system. We can use Spring Boot Actuator along with tools like Prometheus and Grafana to track metrics like saga completion time, failure rates, and compensation actions triggered.

Here's how we might add some custom metrics to our saga orchestrator:

@Service
public class OrderSagaOrchestrator {

    private final Counter sagaCompletions;
    private final Counter sagaFailures;
    private final Timer sagaDuration;

    public OrderSagaOrchestrator(MeterRegistry registry) {
        this.sagaCompletions = registry.counter("order_saga_completions");
        this.sagaFailures = registry.counter("order_saga_failures");
        this.sagaDuration = registry.timer("order_saga_duration");
    }

    public void processOrder(Order order) {
        Timer.Sample sample = Timer.start();
        try {
            // Saga logic here
            sagaCompletions.increment();
        } catch (Exception e) {
            sagaFailures.increment();
            throw e;
        } finally {
            sample.stop(sagaDuration);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

These metrics will give us valuable insights into how our sagas are performing in production.

Implementing sagas in Spring Boot isn't a walk in the park, but it's a powerful tool for managing complex, distributed transactions. By carefully designing our sagas, implementing proper error handling and compensation logic, and setting up robust monitoring, we can build resilient, scalable systems that can handle even the most complex business processes.

Remember, the key to successful saga implementation is thinking through all the possible failure scenarios and how to handle them. It's not just about the happy path, but about gracefully handling all the things that could go wrong in a distributed system.

As you work with sagas, you'll likely encounter challenges like dealing with long-running transactions, handling timeouts, and managing saga recovery after system failures. These are all areas where you'll need to develop strategies specific to your system's needs.

Sagas are a big topic, and there's always more to learn. As you implement them in your Spring Boot applications, you'll discover new patterns and best practices. Keep experimenting, testing, and refining your approach. With time and experience, you'll be able to build incredibly robust and scalable distributed systems using the saga pattern in Spring Boot.


Our Creations

Be sure to check out our creations:

Investor Central | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (2)

Collapse
 
igventurelli profile image
Igor Venturelli

Very nice article, very well explained and easy to understand.

Collapse
 
aaravjoshi profile image
Aarav Joshi

Thank you