Hello guys, managing transaction in a distributed system like Microservices is very challenging and if you don't have a proper approach then you may face production issues and end of with data lose, data corruption and app failure. That's why managing distributed transaction is also an important topic on Microservice interviews. If you are working in Microservices architecture then you may have already heard about the SAGA and 2 Phase Commit etc. and there is also a chance that you may have been asked about those on Java developer interviews. If not, don't worry, in this article, I will tell you about those Microservices design patterns while showing how to manage distributed transactions in Microservices architectures in Java.
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.
1. What are Distributed Transactions in Microservices?
A distributed transaction involves multiple services that need to work together to complete a transaction.In a Microservices architectureMicroservices 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.
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.
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.
Here is a nice diagram which shows how two phase commit works:
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.
Here is a nice diagram which shows how Saga pattern works in a Microservices architecture:
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. This is probably the simplest solution of managing distributed transactions in Microservices architecture.
Here is a nice diagram to show how Event Drive Architecture works:
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)
Here is a sample code of two phase commit using Java:
@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:
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.
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
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.
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.
Other System Design and Microservices Tutorials and Resources
- How to Prepare for System Design Interview?
- 7 Courses to learn Learn Microservices with Spring
- Is ByteByteGo really worth the hype?
- 6 Frameworks for Microservices development in Java
- Top 5 Places to learn System design and Software design
- 5 Courses to learn Quarkus for Microservices in Java
- Is DesignGuru's System Design Course worth it
- 15 Microservices Interview Questions with Answers
- The Complete Java Developer RoadMap
- What is CQRS Design Pattern in Microservices
- Is Exponent's System Design Course worth it?
- How to implement API Gateway using Spring Cloud
- 10 Best Places to Learn System Design
- What is Strangler Pattern in Microservices
- My Favorite Software Design Courses
- How does the Backend for Frontend (BFF) Pattern works?
- 3 Places to Practice System Design Mock interviews
- Why Microservices is not a silver bullet in Java
- 20 System Design Interview Questions for Practice
- Is Designing Data intensive application book worth reading?
Thank you for reading, if you have any questions or doubt feel free to ask in comments.
Any framework which can do this out-of-box for you?
ReplyDelete