Preparing for Java and Spring Boot Interview?

Join my Newsletter, its FREE

Sunday, April 9, 2023

How to Manage Distributed Transaction in Microservices? SAGA and 2 Phase Commit Example Tutorial

Hello guys, managing transaction in a distributed system like Microservices is very challenging and if you don't have a proper approach then you will end of with data lose, data corruption and app failure.That's why distributed transaction is also an important topic on Microservice interviews. There is a good chance that you may have already heard about SAGA and 2 Phase Commit etc. If not, don't worry, in this article, I will tell you how to manage distributed transaction.  In a Microservices architecture, services are designed to be small, autonomous, and loosely coupled. This design philosophy provides many benefits, including scalability, flexibility, and faster development cycles. However, managing transactions across multiple microservices can be a challenging task. In this article, we will explore how to manage distributed transactions in microservices and provide relevant code examples.



How to manage distributed transaction in Microservices?

There are multiple ways to manage transactions in Microservices like applying SAGA Pattern, using 2 Phase commit or using Event Sourcing Design Pattern. In this article, we will understand all of those but before that, let's understand what is distributed transactions and what are challenges related to transaction management in Microservices. 

What are Distributed Transactions in Microservices?

A distributed transaction involves multiple services that need to work together to complete a transaction. In a microservices architecture, each service may have its own database, and transactions that require data from multiple services may need to coordinate the work of all these services to ensure that the transaction is completed successfully. 

For example, when a user makes a purchase, the transaction may involve several services, such as the order service, payment service, and inventory service.


Challenges of Managing Distributed Transactions in Microservices

Managing distributed transactions in a microservices architecture can be challenging due to the following reasons:

1. Complexity
Managing transactions across multiple services can be complex, and coordinating the work of all these services can be challenging.

2. Failure Handling
In a distributed system, failures are inevitable, and handling these failures in a distributed transaction can be challenging.

3. Scalability
Microservices architecture is designed to be scalable, and managing transactions across multiple services can be challenging in a highly scalable environment.

4. Latency
Coordinating the work of multiple services can introduce latency, which can impact the performance of the system.

How to Manage Distributed Transaction in Microservices? SAGA and 2 Phase Commit Example Tutorial


6 Ways to Manage Distributed Transactions in Microservices?

There are several approaches to manage distributed transactions in a microservices architecture. Here are some of the most popular approaches:

1. Two-Phase Commit (2PC)

Two-phase commit is a protocol used to ensure that all participants in a distributed transaction agree to commit or abort the transaction. In this approach, a coordinator service is responsible for coordinating the work of all the services involved in the transaction. 

The coordinator asks all the services to prepare for the transaction, and if all the services are ready, the coordinator sends a commit message to all the services. If any of the services fail to prepare or respond, the coordinator sends an abort message to all the services.

2. Saga Pattern

The Saga pattern is a pattern used to manage long-running transactions in a distributed system. In this pattern, each service involved in the transaction performs a local transaction and sends a message to the next service to perform its transaction. If any of the services fail, the Saga can be rolled back by sending a compensating transaction to undo the work that has already been done.

3. Event-Driven Architecture

Event-driven architecture is an architectural pattern that involves the use of events to trigger actions in a system. In a distributed transaction, each service can publish events when it has completed its part of the transaction. Other services can then subscribe to these events and perform their part of the transaction.


Code Example of SAGA and 2-Phase Commit in Java Microservices

Let's take a look at some code examples of how to manage distributed transactions in microservices using the two-phase commit and Saga pattern approaches.

Two-Phase Commit (2PC)

@Service
public class OrderService {

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private InventoryService inventoryService;

    @Transactional
    public void placeOrder(Order order) throws Exception {
        // Reserve inventory
        inventoryService.reserveInventory(order);

        // Charge payment
        paymentService.chargePayment(order);

        // Commit transaction
        // This is handled by the transaction manager
    }
}

Here is how 2-phase commit look like in a sequence diagram:





2. Saga Pattern
Now that we have seen code example of using 2 phase commit for managing transactions in Microservices, its time to look at SAGA Pattern, another popular ways to manage distributed transactions. 

public class OrderSaga {

    @Autowired
    private OrderService orderService;

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private InventoryService inventoryService;

    @SagaStart
    public


    public void placeOrder(Order order) throws Exception {
        try {
            // Step 1: Reserve inventory
            inventoryService.reserveInventory(order);
            order.setStatus("Inventory Reserved");

            // Step 2: Charge payment
            paymentService.chargePayment(order);
            order.setStatus("Payment Charged");

            // Step 3: Confirm order
            orderService.confirmOrder(order);
            order.setStatus("Order Confirmed");
        } catch (Exception ex) {
            // Step 4: Handle failure
            inventoryService.cancelInventoryReservation(order);
            paymentService.refundPayment(order);
            orderService.cancelOrder(order);
            order.setStatus("Transaction Failed");
        }
    }
}

In addition to the above approaches, there are a few more techniques and best practices that can help manage distributed transactions in microservices. 

Let's take a look at a few of them.

4. Use Idempotent Operations

Idempotent operations are operations that can be repeated without changing the outcome. In a distributed system, using idempotent operations can help ensure that the same operation is not performed twice, even if it is retried due to a failure. 

For example, if a service is trying to update a record in a database, it can check if the record already exists before performing the update operation. If the record already exists, the service can skip the update operation and return a success response.


5. Implement Retry and Timeout Mechanisms

In a distributed system, network failures and timeouts are common. Implementing retry and timeout mechanisms can help handle these failures gracefully. For example, if a service fails to respond to a request, the client can retry the request a few times before giving up. 

Similarly, if a service takes too long to respond, the client can timeout the request and handle the failure gracefully.

How to manage distributed transaction in Microservices?


6. Use a Distributed Transaction Coordinator

A distributed transaction coordinator is a service that manages distributed transactions in a microservices architecture. It provides a centralized mechanism for coordinating the work of all the services involved in a transaction. 

The coordinator can ensure that all the services commit or abort the transaction in a coordinated manner, even in the event of failures.

Let's take a look at a code example that implements idempotent operations and retry mechanisms to handle failures in a distributed transaction.

public class PaymentService {

    @Autowired
    private RestTemplate restTemplate;

    @Value("${inventory.service.url}")
    private String inventoryServiceUrl;

    @Value("${retry.maxAttempts}")
    private int maxAttempts;

    @Value("${retry.backoff}")
    private int backoff;

    @Retryable(value = { HttpClientErrorException.class }, 
maxAttempts = "${retry.maxAttempts}",
backoff = @Backoff(delay = "${retry.backoff}"))
    public void chargePayment(Order order) throws Exception {
        try {
            // Check if payment already exists
            Payment payment = restTemplate.getForObject(
"http://payment-service/payments/{orderId}", 
Payment.class, order.getId());
            if (payment != null) {
                // Payment already exists, skip the operation
                return;
            }

            // Perform payment
            restTemplate.postForObject("http://payment-service/payments",
        order, Payment.class);

        } catch (HttpClientErrorException ex) {
            // Handle 4xx errors
            if (ex.getStatusCode() == HttpStatus.CONFLICT) {
                // Payment already exists, skip the operation
                return;
            }
            throw ex;
        } catch (HttpServerErrorException ex) {
            // Handle 5xx errors
            throw ex;
        }
    }

    @Recover
    public void recoverChargePayment(HttpClientErrorException ex,
   Order order) {
        // Handle retry failure
        // Log the error and throw an exception
        throw new RuntimeException("Failed to charge payment after " 
        + maxAttempts + " retries");
    }
}


Conclusion
That's all about how to manage transactions in Microservices. Managing distributed transactions in a microservices architecture is a complex task that requires careful planning and implementation. In this article, we explored some of the popular approaches and best practices to manage distributed transactions, including idempotent operations, retry and timeout mechanisms, and the use of a distributed transaction coordinator.

You have also see the  code example that implements idempotent operations and retry mechanisms to handle failures in a distributed transaction. By following these approaches and best practices, you can effectively manage distributed transactions in your microservices architecture and ensure that your system is reliable and scalable.

No comments :

Post a Comment