Compared to in-memory calls, network-based communication introduces additional complexity. Remote calls may fail due to timeouts, network instability, or errors in downstream services. In high-traffic, internet-facing systems, such failures can escalate into catastrophic issues and potentially render the system unresponsive. For this reason, the circuit breaker pattern is used to prevent such scenarios.
In practice, the circuit breaker pattern is implemented by wrapping a remote call with a component that monitors failures. When the failure rate exceeds a configured threshold, the circuit breaker transitions to the open state, preventing calls from reaching the protected service.
In this article, we’ll explore some Java-based solutions related to this topic.
Resilience4j
Resilience4j is a lightweight Java library providing fault-tolerance patterns such as circuit breaker, retry, rate limiter, and bulkhead with several modules. (You can find the modules here: Maven Repositories)
Since we are focusing on the circuit breaker pattern, let’s walk through the steps required to use the circuit breaker module from Resilience4j.
To start, we need to add the resilience4j-circuitbreaker dependency to our pom.xml file.
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>2.3.0</version>
</dependency>
Creating a CircuitBreaker Configuration
Next, we need to create an instance of circuit breaker registry. For the example code, we've created circuit breaker registry with a minimal configuration.
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.slidingWindowSize(10)
.minimumNumberOfCalls(5)
.failureRateThreshold(50)
.build();
CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(config);
This configuration means:
- The circuit breaker evaluates the last 10 calls
- At least 5 calls must be recorded before calculating the failure rate
- If 50% or more of the calls fail, the circuit breaker transitions to OPEN
You can find all available configuration options here: https://resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker
Executing The Remote Call
You can create an instance of CircuitBreaker from the registry and execute a remote call using the executeSupplier() method:
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("circuitBreaker");
String result = circuitBreaker.executeSupplier(() -> paymentGateway.getStatus("TRID00001"));
To better understand how the circuit breaker behaves, let’s examine a test scenario.
Scenario 1: The remote service is unreachable from the beginning
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.slidingWindowSize(10)
.minimumNumberOfCalls(5)
.failureRateThreshold(50)
.build();
CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(config);
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("circuitBreaker");
PaymentGateway paymentGateway = mock(PaymentGateway.class);
when(paymentGateway.getStatus(anyString())).thenThrow(new RuntimeException("Failed"));
for (int i = 1; i <= 10; i++) {
try {
String result = circuitBreaker.executeSupplier(
() -> paymentGateway.getStatus("TRID00001")
);
} catch (RuntimeException exception) {
System.out.printf(
"%d) %s:%s%n",
i,
exception.getClass().getSimpleName(),
exception.getMessage()
);
}
}
verify(paymentGateway, times(5)).getStatus(anyString());
Assertions.assertThat(circuitBreaker.getState())
.isEqualTo(CircuitBreaker.State.OPEN);
We'll see the following output:
1) RuntimeException:Failed
2) RuntimeException:Failed
3) RuntimeException:Failed
4) RuntimeException:Failed
5) RuntimeException:Failed
6) CallNotPermittedException:CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
7) CallNotPermittedException:CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
8) CallNotPermittedException:CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
9) CallNotPermittedException:CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
10) CallNotPermittedException:CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
In this test, the circuit breaker is configured with minimumNumberOfCalls(5).
This means the circuit breaker will not make any state transition decisions until at least 5 calls have been recorded.
Because all recorded calls fail and the configured failure rate threshold is 50%, the circuit breaker transitions to the OPEN state immediately after the fifth call. Once open, it prevents further calls from reaching the remote service and throws a CallNotPermittedException instead.
Scenario 2: Failed Calls Exceeds The Failure Rate Threshold And The Remote Service Recover Later
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.slidingWindowSize(10)
.minimumNumberOfCalls(5)
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(500))
.permittedNumberOfCallsInHalfOpenState(2)
.build();
CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(config);
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("circuitBreaker");
PaymentGateway paymentGateway = mock(PaymentGateway.class);
when(paymentGateway.getStatus(anyString()))
.thenReturn("SUCCESS", "SUCCESS", "SUCCESS", "SUCCESS", "SUCCESS")
.thenThrow(
new RuntimeException("Downstream failure"),
new RuntimeException("Downstream failure"),
new RuntimeException("Downstream failure"),
new RuntimeException("Downstream failure"),
new RuntimeException("Downstream failure")
)
.thenReturn("SUCCESS", "SUCCESS");
// 1) First 5 calls succeed
for (int i = 1; i <= 5; i++) {
final String txId = "TX-" + i;
circuitBreaker.executeSupplier(() -> paymentGateway.getStatus(txId));
}
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
// 2) Next 5 calls fail -> OPEN
for (int i = 6; i <= 10; i++) {
final String txId = "TX-" + i;
try {
circuitBreaker.executeSupplier(() -> paymentGateway.getStatus(txId));
} catch (Exception ignored) {}
}
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);
// 3) Wait duration expires
Thread.sleep(600);
// Trigger HALF_OPEN (first permitted call)
circuitBreaker.executeSupplier(() -> paymentGateway.getStatus("TX-11"));
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.HALF_OPEN);
// 4) Second permitted call -> CLOSED
circuitBreaker.executeSupplier(() -> paymentGateway.getStatus("TX-12"));
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
verify(paymentGateway, times(12)).getStatus(anyString());
This example clearly demonstrates how the circuit breaker operates. Let’s review it step by step:
- The mock is configured to return a successful response for the first five calls, throw exceptions for the next five calls, and return successful responses for the final two calls.
- Since the first five calls complete successfully, the circuit breaker remains in the CLOSED state.
- The circuit breaker is configured with a minimum number of 5 calls, a sliding window size of 10, and a failure rate threshold of 50%. Because calls #6 through #10 fail, the failure rate exceeds the threshold and the circuit breaker transitions to the OPEN state.
- The waitDurationInOpenState is configured as 500 ms, so the test waits slightly longer (600 ms) before making another call.
- The first call after the wait duration transitions the circuit breaker from OPEN to HALF_OPEN.
- Since permittedNumberOfCallsInHalfOpenState is set to 2, the second successful call transitions the circuit breaker from HALF_OPEN back to CLOSED.
Decorating the Remote Call
Resilience4j allows us to reuse protected remote service calls by creating decorated functions with a circuit breaker.
Function<String, String> decoratedGetStatus =
CircuitBreaker.decorateFunction(
circuitBreaker,
paymentGateway::getStatus
);
// Call the decorated function
decoratedGetStatus.apply("TRID00001");
If the method to be wrapped accepts multiple arguments:
public interface PaymentGateway {
AuthorizationResult authorize(String token, Integer amount);
}
you can decorate the remote call as shown below:
BiFunction<String, Integer, AuthorizationResult> decoratedAuthorize = (token, amount) -> CircuitBreaker.decorateSupplier(
circuitBreaker,
() -> paymentGateway.authorize(token, amount)
)
.get();
// Call the decorated function
decoratedAuthorize.apply("7cf267eb-21d8-4802-9703-d4309bd3eddc", 100);
Adding Fallback
So far, we have seen how to cut off communication between the application and a remote service under certain circumstances. When the circuit breaker is in the OPEN state, it throws an exception instead of allowing the remote call to proceed. Handling both remote service exceptions and circuit breaker exceptions at every call site can quickly become inconvenient and error-prone.
Function<String, String> safeDecoratedGetStatus = transactionId -> {
try {
return decoratedGetStatus.apply(transactionId); }
catch (Exception exception) {
return "UNKNOWN";
}
};
Instead, we can provide a fallback response when a failure occurs. This approach allows the application to degrade gracefully by returning a predefined or computed fallback value whenever the remote call fails or the circuit breaker is open.
For cleaner and more expressive handling, you can consider using io.vavr:vavr:
Function<String, String> safeDecoratedGetStatus = transactionId ->
Try.of(() -> decoratedGetStatus.apply(transactionId))
.recover(throwable -> "UNKNOWN").get();
Resilience4j offers many useful components in addition to the circuit breaker, and these components can be combined to build more resilient systems. I strongly recommend reviewing the official documentation for more examples and advanced usage: https://resilience4j.readme.io/docs/examples
Spring Cloud Circuit Breaker
Spring Cloud Circuit Breaker is an abstraction layer over multiple circuit breaker libraries, similar to how Spring’s RestClient abstracts HTTP clients.
To start using Spring Cloud Circuit Breaker with Spring Boot, add the following dependency to your pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
<version>5.0.0</version>
</dependency>
Configuring Spring Cloud Circuit Breaker
To define a default configuration that applies to all circuit breakers, you can register a Customizer bean as shown below:
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> circuitBreakerCustomizer(CircuitBreakerConfig defaultCircuitBreakerConfig) {
return factory -> factory.configureDefault(id ->
new Resilience4JConfigBuilder(id)
.circuitBreakerConfig(defaultCircuitBreakerConfig)
.build());
}
@Bean
public CircuitBreakerConfig defaultCircuitBreakerConfig() {
return CircuitBreakerConfig.custom()
.slidingWindowSize(10)
.minimumNumberOfCalls(5)
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(60))
.build();
}
Alternatively, you can configure circuit breakers declaratively using application configuration:
resilience4j.circuitbreaker:
configs:
default:
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
waitDurationInOpenState: 60s
instances:
OrderService:
baseConfig: default
Protecting Services With Circuit Breaker
You can protect service calls by using a CircuitBreakerFactory, which is injected into your service class.
@Service
public class OrderService {
private final CircuitBreakerFactory<?, ?> circuitBreakerFactory;
public OrderService(CircuitBreakerFactory<?, ?> circuitBreakerFactory) {
this.circuitBreakerFactory = circuitBreakerFactory;
}
public OrderStatus getOrderStatus(String orderId) {
return circuitBreakerFactory.create("OrderService")
.run(
() -> callGetStatusApi(orderId),
throwable -> fallbackGetStatus(orderId, throwable)
);
}
private OrderStatus callGetStatusApi(String orderId) {
// TODO: Call the api
return new OrderStatus();
}
private OrderStatus fallbackGetStatus(String orderId, Throwable throwable) {
// TODO: Return fallback result and log the failure
return new OrderStatus();
}
}
In this example, the circuit breaker named OrderService monitors calls to the downstream service. When the breaker is OPEN, calls are short-circuited and the fallback method is executed instead.
The spring-cloud-starter-circuitbreaker integrates multiple Resilience4j resilience patterns, including CircuitBreaker, Retry, and TimeLimiter under the Spring Cloud Circuit Breaker abstraction. This allows applications to address additional edge cases such as retries for transient failures and timeouts for slow responses using a single starter dependency and consistent configuration model.
For more advanced use cases and configuration options, refer to the official documentation linked in the Credits section.
--
Hope you find it helpful. As always, you can find code example in the following repository.
Thanks for reading!
Top comments (0)