The Circuit Breaker pattern is a crucial design pattern for ensuring system stability and fault tolerance in a microservices architecture. Below is a comprehensive overview of the Circuit Breaker pattern and its implementation in Spring Boot using Resilience4j.
What is a Circuit Breaker?
In a microservices environment, if a specific service goes down or becomes slow, the Circuit Breaker temporarily halts calls to that service. This prevents the entire system from crashing or suffering from a cascading failure.
The 3 main states of a Circuit Breaker:
- Closed: This is the normal operating state. All requests pass successfully from one service to another.
- Open: If a service fails repeatedly or exceeds a specific threshold, the circuit trips to the 'Open' state. Requests are no longer sent to the main service; instead, an alternative or fallback response is returned immediately.
- Half-Open: After a set period, the circuit automatically transitions to the 'Half-Open' state. In this state, a limited number of requests are sent to verify whether the remote service has recovered. If they succeed, the circuit returns to the 'Closed' state; if they fail, it reverts to the 'Open' state.
Circuit Breaker Implementation in Spring Boot
Step 1: Adding Dependencies
Spring Boot project's pom.xml
<!-- Resilience4j Spring Boot Starter --> io.github.resilience4j
resilience4j-spring-boot3
<!-- For Spring Boot 3 -->
2.1.0
Application.property file configuration.
Resilience4j Circuit Breaker configuration for userServiceBreaker
resilience4j.circuitbreaker.instances.userServiceBreaker.registerHealthIndicator=true
resilience4j.circuitbreaker.instances.userServiceBreaker.slidingWindowType=COUNT_BASED
resilience4j.circuitbreaker.instances.userServiceBreaker.slidingWindowSize=10
resilience4j.circuitbreaker.instances.userServiceBreaker.failureRateThreshold=50
resilience4j.circuitbreaker.instances.userServiceBreaker.minimumNumberOfCalls=5
resilience4j.circuitbreaker.instances.userServiceBreaker.automaticTransitionFromOpenToHalfOpenEnabled=true
resilience4j.circuitbreaker.instances.userServiceBreaker.waitDurationInOpenState=10s
resilience4j.circuitbreaker.instances.userServiceBreaker.permittedNumberOfCallsInHalfOpenState=3
Code Implementation (Service & Controller):
In Service Class:--------------
We will use the @CircuitBreaker annotation and create a fallback method.
package com.example.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate; @Service
public class OrderService {
private final RestTemplate restTemplate = new RestTemplate();
private static final String USER_SERVICE_URL = "http://localhost:8081/users/";
// The name here must match the instance name defined in application.yml
@CircuitBreaker(name = "userServiceBreaker", fallbackMethod = "getUserFallback")
public String getUserDetails(Long userId) {
// Assume this calls another microservice that might be down
return restTemplate.getForObject(USER_SERVICE_URL + userId, String.class);
}
// The fallback method signature (parameters and return type) must match the original method,
// with the addition of a Throwable or Exception parameter.
public String getUserFallback(Long userId, Throwable throwable) {
return "Sorry, the user service is currently down. Showing fallback data. Reason: " + throwable.getMessage();
}
}
In the Controller class:
package com.example.controller;
import com.example.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/order/user/{id}")
public String getOrderUser(@PathVariable Long id) {
return orderService.getUserDetails(id);
}
}
Testing:----
- Normal State (Closed): When the user service (port: 8081) is active, the request will directly fetch the data.
- Service Down (Open): If the user service is down, errors will occur during the initial attempts. As soon as the failure rate exceeds 50% within 10 requests, the circuit breaker will trip and switch to the 'Open' state. Subsequently, it will execute the
getUserFallbackmethod directly, without making the service call. - Recovery (Half-Open): 10 seconds
Now , we Consolidated all these:---------
Guidelines for combining Retry or Rate Limiter patterns with the Circuit Breaker:---------
Resilience4j allows you to chain or combine multiple patterns (such as Circuit Breaker, Retry, and Rate Limiter) on the same method.
Retry Pattern:------
This is used to automatically retry an operation a few times if a request fails due to temporary network issues or other reasons, rather than immediately returning an error.
We include this in the application.properties file:----------
Resilience4j Retry configuration for userServiceRetry
resilience4j.retry.instances.userServiceRetry.maxAttempts=3
resilience4j.retry.instances.userServiceRetry.waitDuration=2s
Rate Limiter Pattern:
This is used to control the maximum number of API calls a user or client can make.
The application.properties file and settings for Rate Limiter:
Resilience4j Rate Limiter configuration for userServiceRateLimiter
resilience4j.ratelimiter.instances.userServiceRateLimiter.limitForPeriod=5
resilience4j.ratelimiter.instances.userServiceRateLimiter.limitRefreshPeriod=10s
resilience4j.ratelimiter.instances.userServiceRateLimiter.timeoutDuration=0
An important rule (Order) regarding the simultaneous use of multiple annotations in the code:------
Execution sequence of annotations:
Retry ➡️ CircuitBreaker ➡️ RateLimiter
The combined code for all these features looks like this:-----------
package com.example.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.github.resilience4j.retry.annotation.Retry; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class OrderService {
private final RestTemplate restTemplate = new RestTemplate();
// Using three annotations together; all share the same fallback method
@Retry(name = "userServiceRetry", fallbackMethod = "commonFallback")
@CircuitBreaker(name = "userServiceBreaker", fallbackMethod = "commonFallback")
@RateLimiter(name = "userServiceRateLimiter", fallbackMethod = "commonFallback")
public String getUserDetails(Long userId) {
return restTemplate.getForObject("http://localhost:8081/users/" + userId, String.class);
}
// A common fallback method to handle errors for all patterns
public String commonFallback(Long userId, Throwable throwable) {
if (throwable instanceof io.github.resilience4j.ratelimiter.RequestNotPermitted) {
return "You are sending requests too fast! Please wait a moment.";
}
return "The User Service is currently unavailable. Alternative data has been loaded.";
}
}
Hope it will make you understandable.
Top comments (0)