Introduction
Welcome back! In this part, we will talk about part 3 of Microservices in Java, make sure to check part 1 and part 2, let’s dive into it.
In typical microservice architectures, where services are interconnected and continuously communicating, ensuring resilience in Microservices architectures is crucial. However, it’s challenging to guarantee consistent responses from all services. What if a service is down or times out? This could significantly impact business operations and lead to customer complaints. In this article, we will delve into implementing resilience in microservices using Java, focusing on enhancing the robustness and efficiency of your microservice calls, which is essential for maintaining smooth business operations.
Resilience
Resilience refers to the ability of a system to handle and recover from failures gracefully, without affecting the user experience or causing cascading failures throughout the system.
Why is resilience so important?
- Financial Loss: Application downtime or erratic behavior can lead to direct financial loss for businesses.
- Customer Trust: Unreliable services can erode customer trust and satisfaction.
- Brand Reputation: Repeated failures or prolonged downtime can damage a brand’s reputation.
- User Experience: Resilience directly impacts the overall user experience and satisfaction.
- Competitive Advantage: Resilient services can differentiate businesses in competitive markets.
Implementing resilience patterns is crucial for several reasons:
- Enhanced User Experience
- Increased System Availability
- Improved Scalability and Performance
- Simplified Error Handling
In this article, we will explore Java examples to improve the resilience and efficiency of your microservice calls.
Implementing the Circuit Breaker Pattern in Java and Spring
Resilience patterns (Circuit Breaker) are design techniques that help build fault-tolerant and resilient microservices. They provide strategies to handle failures, prevent cascading failures, and ensure the system remains operational and responsive, even when individual services encounter issues.
The Problem
The Solution
As shown in the above diagram, the Circuit Breaker Pattern is a technique used to prevent system failures. Here’s how it works:
- Closed state: normal system operation.
- Open state: The system stops accepting requests to prevent more issues.
- Half-open state: The system partially reopens to test if the issue is resolved.
- Return to Closed: The system resumes normal operation if the test succeeds.
- Remain open: if the test fails, the system continues to cool down before testing again.
This pattern prevents the system from overloading by managing the flow of requests during errors or high traffic.
Pros
- fails fast to prevent cascading failures
- Lets failing dependencies recover
Cons
- Adds overhead and complexity
- Requires tuning failure thresholds
When to Use It
- When a service depends on a remote service it may experience failures or high latency.
- When the remote service is not critical of the application’s core functionality, a fallback response is acceptable.
- When the system needs to maintain stability and prevent cascading failures.
So far we have a good understanding of the issue and how to fix it, now let’s see this in action using spring.
Implementing with Spring 3.2
In the following example, we will demonstrate how the order service processes orders by verifying the availability of the product and quantity in the inventory service. Both services are accessible in the GitHub repository. (inventory-service, order-service).
Note: If you want to see how it works, stop the inventory service and call the order service. After that, the fallback method will be executed.
1- Add the required dependency
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId> </dependency>
@CircuitBreaker(name = "inventoryService", fallbackMethod = "fallbackCheckInventory") public void processOrder(Long productId, long quantity) { System.out.println("processOrder -- calling the inventoryService with productId: "+productId + ", and quantity: "+quantity); Boolean isProductAvailable = inventoryClient.checkProductAvailability(productId); if (!isProductAvailable) { throw new ProductAvailabilityException("Product:"+ productId+" unavailable"); } }
In the above example, the @CircuitBreaker annotation wraps the call checkProductAvailability inside a Circuit Breaker named fallbackCheckInventory handles exceptions by returning a fallback response.
public void fallbackCheckInventory(Long productId, long quantity, Throwable throwable) { System.out.println("Fallback logic if Inventory Service fails for productId " + productId + " and quantity : "+quantity+ ". Error: " + throwable.getMessage()); }
The output
Happy scenario:
When bother services are running (Order service and Inventory service) we will get the response back normally from the Inventory service while executing this API in the postman
curl --location 'http://localhost:8080/api/orders' \ --header 'Content-Type: application/json' \ --data '{ "productId" : 2922, "quantity": 2 }'
Order Service logs:
processOrder -- calling the inventoryService with productId: 2922, and quantity: 2 Order processed successfully
Inventory Service Logs
inventory service --- product id: 2922
Now, let’s assume that the Inventory service is down (we will shut it down). The Order service depends on the inventory service for processing the order. What will happen now? Will the order crash? Let’s review the logs from both services
Order Service logs.
processOrder -- calling the inventoryService with productId: 2922, and quantity: 2 Fallback logic if Inventory Service fails for productId 2922 and quantity : 2. Error: Connection refused executing GET http://localhost:8081/api/inventory/check/2922 Order processed successfully
The logs above indicate that the order did not fail even though the inventory service was down. Instead, it intelligently executed the fallback method and proceeded with the order. This is just an example, and the appropriate response to failure depends on your specific requirements.
Inventory Service logs
There are no logs because it is already down 🙂
Conclusion
Resilience is a critical aspect of building robust and reliable microservice architectures. By implementing resilience patterns like the Circuit Breaker, you can improve the stability, fault tolerance, and overall user experience of your system.
The Spring Framework provides excellent support for implementing the Circuit Breaker pattern through the@CircuitBreaker annotation and integration with the Resilience4j library. By following the steps outlined in this article, you can easily incorporate Circuit Breakers into your Spring-based microservices and enhance their resilience.
Remember, while the Circuit Breaker pattern is a powerful tool, it is just one of many resilience patterns available. It’s important to consider other patterns like Retry, Fallback, Bulkhead, and Rate Limiting based on your specific requirements and the characteristics of your system.
By embracing resilience and implementing appropriate patterns, you can build microservices that are capable of handling failures gracefully, maintaining system availability, and providing a seamless user experience even in the face of adversity.