Circuit Breaker in Spring Boot (Spring Cloud Circuit Breaker + Resilience4j)
- 4.5/5
- 4228
- Oct 30, 2024
What is circuit breaker pattern in microservices?
The circuit breaker pattern in microservices is a design pattern that helps prevent cascading failures in distributed systems by stopping or breaking the connection to a failing service or component after a threshold of failures is reached.
How It Works?
The circuit breaker monitors the number of failures or error rates over time and has three main states:
Closed: Requests are routed normally. The circuit breaker counts the number of recent failures. If these failures exceed a certain threshold within a defined window, the breaker switches to the Open state.
Open: Requests to the failing service are stopped immediately. Instead of making a call that’s likely to fail, the circuit breaker immediately returns an error or executes a fallback (alternative response). After a cooldown period, it moves to the Half-Open state.
Half-Open: A limited number of requests are allowed through to test if the service has recovered. If requests succeed, the circuit breaker resets to Closed; otherwise, it returns to Open.
What is Spring Cloud Circuit Breaker?
Spring Cloud Circuit Breaker is a library that provides an easy-to-use abstraction layer for implementing circuit breakers in Spring Boot applications, offering seamless integration with popular resilience libraries like Resilience4j and Spring Retry.
The following starters are available with the Spring Cloud BOM:
1) Resilience4j: org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j
2) Reactive Resilience4j: org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j
3) Spring Retry: org.springframework.cloud:spring-cloud-starter-circuitbreaker-spring-retry
In this article, we implement a circuit breaker with Resilience4j using org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j.
How to Implement Spring Cloud Circuit Breaker with Resilience4j?
Usecase
Imagine a microservices-based e-commerce application where an order-service relies on an inventory-service. If the "inventory-service" becomes slow or fails completely, the circuit breaker in "order-service" will "open" after several failed requests, stopping further requests to "inventory-service".
The "order-service" can then provide a fallback response (like 'inventory data is currently unavailable') to prevent impacting the end-user experience.
Implementation
inventory-service
This is a simple Spring Boot web application with only one endpoint, so there is no need to explain it here.
The source code for this application can be found here: Github.
order-service
Now, let's implement Spring Cloud Circuit Breaker using Resilience4j in the order-service, which calls inventory-service with a count-based circuit breaker.
1) Use Spring Initializr (Web Interface)
1.1) Open Spring Initializr in your browser.
1.2) Choose either Gradle - Groovy or Gradle - Kotlin as the build tool, based on your preferred language.
1.3) Select dependencies for your project, including Spring Web, Spring Boot Actuator, Resilience4J, and any other required dependencies.
1.4) Click Generate to download the project as a ZIP file.
1.5) Unzip the downloaded file, and open it in your preferred IDE, such as IntelliJ IDEA or VS Code.
2) Add Dependencies
Add the necessary dependencies to your build.gradle file. The final configuration should look something like this:
plugins { | |
id 'java' | |
id 'org.springframework.boot' version '3.3.4' | |
id 'io.spring.dependency-management' version '1.1.6' | |
} | |
group = 'com.cb' | |
version = '0.0.1-SNAPSHOT' | |
java { | |
toolchain { | |
languageVersion = JavaLanguageVersion.of(21) | |
} | |
} | |
repositories { | |
mavenCentral() | |
} | |
ext { | |
set('springCloudVersion', "2023.0.3") | |
} | |
dependencies { | |
implementation 'org.springframework.boot:spring-boot-starter-actuator' | |
implementation 'org.springframework.boot:spring-boot-starter-web' | |
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j' | |
implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.6.0' | |
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-aop' | |
testImplementation 'org.springframework.boot:spring-boot-starter-test' | |
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' | |
compileOnly 'org.projectlombok:lombok' | |
annotationProcessor 'org.projectlombok:lombok' | |
} | |
dependencyManagement { | |
imports { | |
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" | |
} | |
} | |
tasks.named('test') { | |
useJUnitPlatform() | |
} |
The spring-boot-starter-web dependency is used for building REST APIs. The spring-boot-starter-actuator dependency enables monitoring and management endpoints. The optional Lombok dependencies help reduce boilerplate code. The optional spring-boot-devtools dependency provides hot reloading to enhance development productivity.
3) Configure Properties
In your application.yml, add the following configurations for the inventory-service URL, Resilience4j circuit breaker, Swagger UI, and Actuator to expose the circuit breaker health endpoint.
spring: | |
application: | |
name: order-service | |
# Configuration for the inventory service | |
inventory: | |
service: | |
url: http://localhost:8081/inventory/ | |
# Configuration for resilience4j circuit breaker | |
resilience4j: | |
circuitbreaker: | |
instances: | |
inventory-circuit-breaker: | |
registerHealthIndicator: true | |
slidingWindowType: COUNT_BASED | |
slidingWindowSize: 10 | |
failureRateThreshold: 50 | |
waitDurationInOpenState: 5000 # 5 seconds | |
permittedNumberOfCallsInHalfOpenState: 3 | |
automaticTransitionFromOpenToHalfOpenEnabled: true | |
slowCallRateThreshold: 50 | |
slowCallDurationThreshold: 2000 # 2 seconds | |
# Configuration for Swagger UI | |
springdoc: | |
swagger-ui: | |
path: /swagger-ui.html | |
# Configuration for Spring Boot Actuator | |
management: | |
endpoints: | |
web: | |
exposure: | |
include: "*" | |
health: | |
show-details: always | |
health.circuitbreakers.enabled: true | |
# logging configuration | |
logging: | |
level: | |
io: | |
github: | |
resilience4j: | |
circuitbreaker: DEBUG |
4) Order Model
The Order model represents an order in the order-service. It includes basic fields like orderId, itemId, itemName, and quantity.
package com.cb.model; | |
/** | |
* Record representing an Order. | |
* | |
* @param orderId the ID of the order | |
* @param itemId the ID of the item in the order | |
* @param itemName the name of the item in the order | |
* @param quantity the quantity of the item in the order | |
*/ | |
public record Order(String orderId, String itemId, String itemName, int quantity) { | |
} |
5) InventoryResponse Model
The InventoryResponse model represents the response from the inventory-service, containing fields for product availability status and possibly additional details.
package com.cb.client.response; | |
/** | |
* A record representing the response from the inventory service. | |
* | |
* @param id the ID of the inventory item | |
* @param name the name of the inventory item | |
* @param quantity the quantity of the inventory item | |
*/ | |
public record InventoryResponse(String id, String name, int quantity) { | |
} |
6) Create InventoryServiceClient
Define a RestClient that will make a REST call to the inventory-service. Use the @CircuitBreaker annotation provided by Spring Cloud Circuit Breaker to apply the circuit breaker to the method.
package com.cb.client; | |
import com.cb.client.response.InventoryResponse; | |
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.springframework.beans.factory.annotation.Value; | |
import org.springframework.stereotype.Component; | |
import org.springframework.web.client.RestTemplate; | |
/** | |
* Client for interacting with the inventory service. | |
*/ | |
@Component | |
public class InventoryClient { | |
private static final Logger logger = LoggerFactory.getLogger(InventoryClient.class); | |
private final RestTemplate restTemplate; | |
private final String inventoryServiceUrl; | |
/** | |
* Constructs an InventoryClient with the specified RestTemplate and inventory service URL. | |
* | |
* @param restTemplate the RestTemplate to use for HTTP requests | |
* @param inventoryServiceUrl the base URL of the inventory service | |
*/ | |
public InventoryClient(RestTemplate restTemplate, @Value("${inventory.service.url}") String inventoryServiceUrl) { | |
this.restTemplate = restTemplate; | |
this.inventoryServiceUrl = inventoryServiceUrl; | |
} | |
/** | |
* Fetches an inventory item by its ID, with circuit breaker support. | |
* | |
* @param itemId the ID of the inventory item to fetch | |
* @return the fetched InventoryResponse, or null if the request fails | |
*/ | |
@CircuitBreaker(name = "inventory-circuit-breaker", fallbackMethod = "fallbackGetInventoryItem") | |
public InventoryResponse getInventoryItem(String itemId) { | |
String url = inventoryServiceUrl + itemId; | |
logger.info("Fetching inventory item from URL: {}", url); | |
var response = restTemplate.getForObject(url, InventoryResponse.class); | |
logger.info("Received inventory item: {}", response); | |
return response; | |
} | |
/** | |
* Fallback method for getInventoryItem, called when the circuit breaker is open or an error occurs. | |
* | |
* @param itemId the ID of the inventory item that was attempted to be fetched | |
* @param t the throwable that caused the fallback | |
* @return null, or a default InventoryResponse | |
*/ | |
public InventoryResponse fallbackGetInventoryItem(String itemId, Throwable t) { | |
logger.error("Error fetching inventory for itemId: {}, so falling back to default response", itemId, t); | |
// or you can return a default InventoryResponse | |
return new InventoryResponse("default", "Default Item", 0); | |
} | |
} |
The @CircuitBreaker annotation applies the circuit breaker to the getInventoryItem method.
The fallback method is called when the circuit breaker is open or the call fails.
7) Register RestTemplate Bean
Ensure a RestTemplate bean is available for dependency injection in your InventoryClient:
package com.cb.client.conf; | |
import org.springframework.context.annotation.Bean; | |
import org.springframework.context.annotation.Configuration; | |
import org.springframework.web.client.RestTemplate; | |
/** | |
* Configuration class for client-related beans. | |
*/ | |
@Configuration | |
public class ClientConfig { | |
/** | |
* Creates a {@link RestTemplate} bean. | |
* | |
* @return a new instance of {@link RestTemplate} | |
*/ | |
@Bean | |
public RestTemplate restTemplate() { | |
return new RestTemplate(); | |
} | |
} |
8) Use InventoryClient in the OrderController
Now, inject InventoryClient in OrderController and use it to call the inventory-service.
package com.cb.controller; | |
import com.cb.client.InventoryClient; | |
import com.cb.model.Order; | |
import org.springframework.web.bind.annotation.GetMapping; | |
import org.springframework.web.bind.annotation.PathVariable; | |
import org.springframework.web.bind.annotation.RestController; | |
/** | |
* REST controller for handling order-related requests. | |
*/ | |
@RestController | |
public class OrderController { | |
private final InventoryClient inventoryClient; | |
public OrderController(InventoryClient inventoryClient) { | |
this.inventoryClient = inventoryClient; | |
} | |
/** | |
* Retrieves an order by its ID. | |
* | |
* @param orderId the ID of the order to retrieve | |
* @return the Order object containing order details | |
*/ | |
@GetMapping("/order/{orderId}") | |
public Order getOrder(@PathVariable String orderId) { | |
// Dummy itemId for demonstration purposes | |
var itemId = "dummyItemId"; | |
var inventoryResponse = inventoryClient.getInventoryItem(itemId); | |
// Create a dummy Order Response using the inventory data | |
return new Order(orderId, itemId, inventoryResponse.name(), inventoryResponse.quantity()); | |
} | |
} |
9) Run the Application
Start your Spring Boot application. After starting, the Swagger UI should be accessible at: http://localhost:8080/swagger-ui.html
10) Verify Circuit Breaker Behavior
Using Spring Boot Actuator, you can view circuit breaker metrics at the /actuator/circuitbreakers endpoint.
Trigger a few failed responses by having the inventory-service throw a RuntimeException or by shutting down the service. Once the configured failure threshold is reached, the circuit breaker will open, causing subsequent calls to return the fallback response.
After enough time has passed (e.g., 60 seconds, as per your configuration), the circuit breaker transitions to the half-open state.
As we have set the logging level for "io.github.resilience4j.circuitbreaker" to DEBUG, this will allow us to see the following informative logs in the application logs.
2024-10-28T01:16:15.470+05:30 DEBUG 6115 --- [order-service] [io-8080-exec-10] i.g.r.c.i.CircuitBreakerStateMachine : CircuitBreaker 'inventory-circuit-breaker' recorded an exception as failure: . . . . 2024-10-28T01:16:15.923+05:30 DEBUG 6115 --- [order-service] [nio-8080-exec-1] i.g.r.c.i.CircuitBreakerStateMachine : Event ERROR published: 2024-10-28T01:16:15.923560+05:30[Asia/Kolkata]: CircuitBreaker 'inventory-circuit-breaker' recorded an error: 'org.springframework.web.client.HttpServerErrorException$InternalServerError: 500 : "{"timestamp":"2024-10-27T19:46:15.920+00:00","status":500,"error":"Internal Server Error","path":"/inventory/dummyItemId"}"'. Elapsed time: 7 ms 2024-10-28T01:16:15.924+05:30 DEBUG 6115 --- [order-service] [nio-8080-exec-1] i.g.r.c.i.CircuitBreakerStateMachine : Event FAILURE_RATE_EXCEEDED published: 2024-10-28T01:16:15.924930+05:30[Asia/Kolkata]: CircuitBreaker 'inventory-circuit-breaker' exceeded failure rate threshold. Current failure rate: 100.0 2024-10-28T01:16:15.930+05:30 DEBUG 6115 --- [order-service] [nio-8080-exec-1] i.g.r.c.i.CircuitBreakerStateMachine : Event STATE_TRANSITION published: 2024-10-28T01:16:15.930455+05:30[Asia/Kolkata]: CircuitBreaker 'inventory-circuit-breaker' changed state from CLOSED to OPEN . . . 2024-10-28T01:16:20.939+05:30 DEBUG 6115 --- [order-service] [ransitionThread] i.g.r.c.i.CircuitBreakerStateMachine : Event STATE_TRANSITION published: 2024-10-28T01:16:20.938948+05:30[Asia/Kolkata]: CircuitBreaker 'inventory-circuit-breaker' changed state from OPEN to HALF_OPEN
Unit Testing Circuit Breaker
This test class, InventoryClientTest, verifies the behavior of the InventoryClient class in different circuit breaker states:
package com.cb.client; | |
import com.cb.Application; | |
import com.cb.client.response.InventoryResponse; | |
import io.github.resilience4j.circuitbreaker.CallNotPermittedException; | |
import io.github.resilience4j.circuitbreaker.CircuitBreaker; | |
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; | |
import org.junit.jupiter.api.BeforeEach; | |
import org.junit.jupiter.api.Test; | |
import org.junit.jupiter.api.parallel.Execution; | |
import org.junit.jupiter.api.parallel.ExecutionMode; | |
import org.mockito.MockitoAnnotations; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.beans.factory.annotation.Value; | |
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; | |
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; | |
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; | |
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; | |
import org.springframework.boot.test.context.SpringBootTest; | |
import org.springframework.boot.test.mock.mockito.MockBean; | |
import org.springframework.web.client.RestTemplate; | |
import static org.junit.jupiter.api.Assertions.assertEquals; | |
import static org.junit.jupiter.api.Assertions.assertNotNull; | |
import static org.mockito.ArgumentMatchers.anyString; | |
import static org.mockito.Mockito.*; | |
/** | |
* Test class for InventoryClient. | |
*/ | |
@SpringBootTest(classes = Application.class) | |
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, | |
DataSourceTransactionManagerAutoConfiguration.class, | |
HibernateJpaAutoConfiguration.class}) | |
@Execution(ExecutionMode.SAME_THREAD) | |
class InventoryClientTest { | |
@MockBean | |
private RestTemplate restTemplate; | |
@Value("${inventory.service.url}") | |
private String inventoryServiceUrl; | |
@MockBean | |
private CircuitBreaker circuitBreaker; | |
@Autowired | |
private InventoryClient inventoryClient; | |
@Autowired | |
private CircuitBreakerRegistry circuitBreakerRegistry; | |
/** | |
* Sets up the test environment before each test. | |
*/ | |
@BeforeEach | |
void setUp() { | |
MockitoAnnotations.openMocks(this); | |
} | |
/** | |
* Tests that getInventoryItem returns the item when the circuit breaker is closed. | |
*/ | |
@Test | |
void getInventoryItem_ReturnsItem_WhenCircuitBreakerClosed() { | |
circuitBreakerRegistry.circuitBreaker("inventory-circuit-breaker") | |
.transitionToClosedState(); | |
when(restTemplate.getForObject("http://localhost:8081/inventory/1", InventoryResponse.class)) | |
.thenReturn(new InventoryResponse("1", "Item 1", 10)); | |
InventoryResponse result = inventoryClient.getInventoryItem("1"); | |
assertEquals("1", result.id(), "Item ID should match"); | |
verify(restTemplate, times(1)).getForObject(anyString(), eq(InventoryResponse.class)); | |
} | |
/** | |
* Tests that getInventoryItem returns the default item when the circuit breaker is open. | |
*/ | |
@Test | |
void getInventoryItem_ReturnsDefault_WhenCircuitBreakerOpen() { | |
circuitBreakerRegistry.circuitBreaker("inventory-circuit-breaker") | |
.transitionToOpenState(); | |
doThrow(new RuntimeException()).when(restTemplate).getForObject("http://localhost:8081/inventory/1", InventoryResponse.class); | |
InventoryResponse result = null; | |
try { | |
result = inventoryClient.getInventoryItem("1"); | |
} catch (RuntimeException e) { | |
assertEquals(CallNotPermittedException.class, e.getClass(), "Circuit breaker should prevent the request"); | |
verifyNoInteractions(restTemplate, "Request should not be made when circuit breaker is open"); | |
} finally { | |
assertNotNull(result, "Result should not be null"); | |
assertEquals("default", result.id(), "Default item should be returned"); | |
} | |
} | |
/** | |
* Tests that getInventoryItem returns the item when the circuit breaker is half-open. | |
*/ | |
@Test | |
void getInventoryItem_ReturnsItem_WhenCircuitBreakerHalfOpen() { | |
circuitBreakerRegistry.circuitBreaker("inventory-circuit-breaker") | |
.transitionToOpenState(); | |
circuitBreakerRegistry.circuitBreaker("inventory-circuit-breaker") | |
.transitionToHalfOpenState(); | |
doThrow(new RuntimeException()).when(restTemplate).getForObject("http://localhost:8081/inventory/1", InventoryResponse.class); | |
InventoryResponse result = null; | |
try { | |
result = inventoryClient.getInventoryItem("1"); | |
} catch (RuntimeException e) { | |
assertEquals(CallNotPermittedException.class, e.getClass(), "Circuit breaker should prevent the request"); | |
verifyNoInteractions(restTemplate, "Request should not be made when circuit breaker is open"); | |
} finally { | |
assertNotNull(result, "Result should not be null"); | |
assertEquals("default", result.id(), "Default item should be returned"); | |
} | |
} | |
} |
Source code: GitHub