Circuit Breaker Pattern in Microservices
In a microservices system, services constantly call each other over the network. Sooner or later, one of them will become slow or stop responding. The Circuit Breaker pattern stops that one problem from spreading and taking down every service that depends on it.
This tutorial explains how a circuit breaker works, walks through a real e-commerce example, and then shows how to build one in Spring Boot with Resilience4j, including configuration, fallbacks, monitoring and testing.
1. The Problem: Cascading Failures
Splitting an application into microservices means a failure in one service no longer crashes the whole codebase. But it adds a new risk: services depend on each other over the network, and a slow dependency can quietly drag its callers down with it.
Here is how it happens:
- Service C becomes slow, for example because its database is overloaded.
- Service B calls C and waits. Each waiting request holds a thread.
- New requests keep arriving, and soon all of B’s threads are stuck waiting for C.
- B can no longer answer anyone, so Service A, which calls B, starts waiting too.
One slow service has now made three services unavailable. This chain reaction is called a cascading failure, and it is exactly what the circuit breaker is designed to stop.
2. What Is the Circuit Breaker Pattern?
The pattern is named after the circuit breaker in your home’s electrical panel. When too much current flows, the breaker “trips” and cuts the power, protecting the wiring from damage. Once the problem is fixed, you switch it back on.
A software circuit breaker sits between your service and a remote dependency. It watches every call. When too many calls fail, it trips and stops sending requests to that dependency for a while, returning a quick error or a fallback response instead.
| Electrical circuit breaker | Software circuit breaker |
|---|---|
| Monitors the electrical current | Monitors calls to a remote service |
| Trips when the current is too high | Opens when too many calls fail or are too slow |
| Cuts power to protect the wiring | Blocks calls to protect both services |
| Is reset once the fault is fixed | Tests the service and closes again once it recovers |
The goal is simple: fail fast instead of failing slowly. A quick, clear failure is far better than threads piling up while they wait for a service that will not answer.
3. The Three States of a Circuit Breaker
A circuit breaker is a small state machine with three main states:
| State | What happens to calls | Moves to |
|---|---|---|
| CLOSED (normal) | Calls go through to the remote service. The breaker records successes and failures. | OPEN when the failure rate passes the threshold |
| OPEN (tripped) | Calls are rejected immediately, without contacting the service. A fallback runs instead. | HALF-OPEN after a waiting period |
| HALF-OPEN (testing) | A small number of trial calls are allowed through to check whether the service has recovered. | CLOSED if the trial calls succeed, or back to OPEN if they fail |
failure rate too high
CLOSED ─────────────────────────▶ OPEN
▲ │
│ │ wait duration ends
│ trial calls succeed ▼
└──────────────────────────── HALF-OPEN
│
OPEN ◀────────────────┘
trial calls fail
Note that “closed” means calls flow, just like a closed electrical circuit lets current flow. “Open” means the circuit is broken and calls are blocked. Resilience4j also has special states such as FORCED_OPEN and DISABLED, which operators can set by hand.
4. A Practical Example: E-commerce Application
Consider an online shop with three services:
| Service | Responsibility |
|---|---|
| Order Service | Creates customer orders |
| Payment Service | Charges the customer; an important but sometimes unreliable remote dependency |
| Inventory Service | Tracks product stock levels |
4.1 Without a circuit breaker
The Payment Service develops a database problem and starts taking 30 seconds to respond:
- The first order request waits 30 seconds for the payment call to time out.
- Meanwhile, new orders keep arriving, and each one also waits on the Payment Service.
- Within moments, every thread in the Order Service is blocked.
- The Order Service can no longer respond to anything, not even health checks or simple order look-ups, although its own code is perfectly healthy.
4.2 With a circuit breaker
Now a circuit breaker wraps the call to the Payment Service:
- CLOSED: payments work normally and the breaker records each result.
- CLOSED → OPEN: the Payment Service slows down. Of the last 10 calls, 6 fail or exceed the slow-call limit. That 60% failure rate passes the 50% threshold, so the breaker opens.
- OPEN: new payment calls are rejected instantly, with no waiting. The Order Service runs a fallback: it saves the order as “Payment Pending” and tells the customer, “Your order is saved. We will confirm payment shortly.” Everything else in the Order Service keeps working.
- OPEN → HALF-OPEN: after 30 seconds, the breaker lets 3 trial payment calls through.
- HALF-OPEN → CLOSED: the trial calls succeed, so the breaker closes and normal payments resume. If they had failed, it would open again for another 30 seconds.
The shop stays online and orders are not lost. The Payment Service also gets breathing room to recover, instead of being flooded with requests while it is struggling.
5. Implementing a Circuit Breaker with Resilience4j
Resilience4j is a lightweight, open-source fault-tolerance library for Java and the most common choice for Spring Boot. It replaced Netflix Hystrix, which is no longer developed.
5.1 Add the dependencies
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>${resilience4j.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
The AOP starter lets the annotations work, and Actuator adds health and metrics endpoints. Set resilience4j.version to the latest release in your pom.xml properties.
5.2 Configure the circuit breaker
resilience4j:
circuitbreaker:
instances:
paymentService:
registerHealthIndicator: true
slidingWindowType: COUNT_BASED
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
slowCallDurationThreshold: 2s
slowCallRateThreshold: 50
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
recordExceptions:
- org.springframework.web.client.ResourceAccessException
- org.springframework.web.client.HttpServerErrorException
ignoreExceptions:
- com.javabytechie.shop.payment.PaymentDeclinedException
In plain words: look at the last 10 calls; once at least 5 have been made, open the circuit if half of them failed or took longer than 2 seconds; stay open for 30 seconds; then allow 3 trial calls. Each setting is explained in the next section.
5.3 Protect the remote call
package com.javabytechie.shop.payment;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class PaymentClient {
private static final Logger log = LoggerFactory.getLogger(PaymentClient.class);
private final RestClient restClient;
public PaymentClient(RestClient.Builder builder) {
this.restClient = builder.baseUrl("http://payment-service").build();
}
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResult charge(PaymentRequest request) {
return restClient.post()
.uri("/api/payments")
.body(request)
.retrieve()
.body(PaymentResult.class);
}
// Same return type and parameters, plus the exception as the last parameter
private PaymentResult paymentFallback(PaymentRequest request, Throwable ex) {
log.warn("Payment unavailable for order {}: {}", request.orderId(), ex.getMessage());
return PaymentResult.pending(request.orderId());
}
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) {
this.orderRepository = orderRepository;
this.paymentClient = paymentClient;
}
@Transactional
public Order placeOrder(OrderRequest request) {
Order order = orderRepository.save(Order.newOrder(request));
PaymentResult payment = paymentClient.charge(PaymentRequest.from(order));
if (payment.isPending()) {
order.markPaymentPending(); // a background job retries these later
} else {
order.markPaid(payment.transactionId());
}
return order;
}
}
The name in @CircuitBreaker must match the instance name in application.yml. While the circuit is open, the fallback receives a CallNotPermittedException, which tells you the call was blocked by the breaker rather than failed by the service.
6. Configuration Explained
| Setting | What it means | Default |
|---|---|---|
slidingWindowType | Judge the last N calls (COUNT_BASED) or the last N seconds (TIME_BASED) | COUNT_BASED |
slidingWindowSize | How many calls (or seconds) to look at | 100 |
minimumNumberOfCalls | Calls needed before the failure rate is calculated at all | 100 |
failureRateThreshold | Failure percentage that opens the circuit | 50 |
slowCallDurationThreshold | Calls slower than this count as slow | 60s |
slowCallRateThreshold | Slow-call percentage that opens the circuit | 100 |
waitDurationInOpenState | How long to stay open before testing again | 60s |
permittedNumberOfCallsInHalfOpenState | Trial calls allowed in half-open state | 10 |
automaticTransitionFromOpenToHalfOpenEnabled | Move to half-open on a timer, without waiting for a new call | false |
recordExceptions | Exceptions that count as failures | All exceptions |
ignoreExceptions | Exceptions that count as neither success nor failure | None |
The defaults are cautious, so most applications tune them. Two settings deserve special attention:
minimumNumberOfCallsstops the circuit from opening because of one or two unlucky calls, for example 1 failure out of 1 call, which is a 100% failure rate.ignoreExceptionsshould list business errors, such as a declined card. A declined card means the Payment Service is working correctly, so it must not push the circuit towards opening.
You can share settings between several instances using configs.default, then override only what differs per instance:
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 20
minimumNumberOfCalls: 10
failureRateThreshold: 50
waitDurationInOpenState: 30s
instances:
paymentService:
baseConfig: default
waitDurationInOpenState: 60s
inventoryService:
baseConfig: default
7. Fallback Strategies
A fallback decides what the user gets when the real call cannot be made. The right choice depends on the feature:
| Strategy | Example |
|---|---|
| Return cached data | Show the last known product price or stock level |
| Return a safe default | Show general recommendations instead of personalised ones |
| Save and process later | Mark the order “Payment Pending” and retry in the background |
| Degrade the feature | Hide the reviews section while the review service is down |
| Fail with a clear message | “Payments are temporarily unavailable. Please try again in a few minutes.” |
Here, a product service falls back to cached data:
@Component
public class InventoryClient {
private final RestClient restClient;
private final StockCache stockCache;
public InventoryClient(RestClient.Builder builder, StockCache stockCache) {
this.restClient = builder.baseUrl("http://inventory-service").build();
this.stockCache = stockCache;
}
@CircuitBreaker(name = "inventoryService", fallbackMethod = "cachedStock")
public StockLevel getStock(String sku) {
StockLevel stock = restClient.get()
.uri("/api/stock/{sku}", sku)
.retrieve()
.body(StockLevel.class);
stockCache.put(sku, stock); // keep a copy for bad times
return stock;
}
private StockLevel cachedStock(String sku, CallNotPermittedException ex) {
// circuit is open: use the last known value
return stockCache.get(sku).orElse(StockLevel.unknown(sku));
}
private StockLevel cachedStock(String sku, Exception ex) {
// the call itself failed: same answer
return stockCache.get(sku).orElse(StockLevel.unknown(sku));
}
}
You can define several fallback methods with different exception types. Resilience4j picks the one whose exception parameter matches most closely, so you can react differently to “circuit open” and “call failed”.
8. Combining with Retry, Timeout and Bulkhead
A circuit breaker works best alongside other resilience patterns. Each one solves a different problem:
| Pattern | Protects against | Learn more |
|---|---|---|
| Circuit Breaker | Repeatedly calling a service that is down | This page |
| Retry | Short, temporary glitches | Retry Pattern |
| Timeout (TimeLimiter) | Waiting too long for one call | Timeout Pattern |
| Bulkhead | One dependency using up all threads | Bulkhead Pattern |
| Rate Limiter | Sending more requests than allowed | Rate Limiting Pattern |
Resilience4j lets you stack them on one method:
@Retry(name = "paymentService")
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
@Bulkhead(name = "paymentService")
public PaymentResult charge(PaymentRequest request) {
return restClient.post()
.uri("/api/payments")
.body(request)
.retrieve()
.body(PaymentResult.class);
}
resilience4j:
retry:
instances:
paymentService:
maxAttempts: 3
waitDuration: 500ms
ignoreExceptions:
- io.github.resilience4j.circuitbreaker.CallNotPermittedException
bulkhead:
instances:
paymentService:
maxConcurrentCalls: 20
By default, Resilience4j applies them in this order, from outside to inside:
Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( your method ) ) ) ) )
Because Retry is outermost, each retry attempt passes through the circuit breaker and is counted. Ignoring CallNotPermittedException in Retry is important: when the circuit is open, retrying is pointless and only adds delay.
Always set network timeouts as well, either on the HTTP client or with a TimeLimiter. Without them, a “slow” call might hang far longer than slowCallDurationThreshold.
9. Monitoring Circuit Breakers
Expose the circuit breaker details through Actuator:
management:
endpoints:
web:
exposure:
include: health,circuitbreakers,circuitbreakerevents,metrics
endpoint:
health:
show-details: always
health:
circuitbreakers:
enabled: true
| Endpoint | Shows |
|---|---|
GET /actuator/health | Each circuit breaker with its state and failure rate |
GET /actuator/circuitbreakers | All circuit breakers and their current state |
GET /actuator/circuitbreakerevents | Recent events: successes, errors, state changes |
GET /actuator/metrics/resilience4j.circuitbreaker.state | Current state as a number, for dashboards |
A response from the health endpoint looks like this:
{
"status": "UP",
"components": {
"circuitBreakers": {
"status": "UP",
"details": {
"paymentService": {
"status": "UP",
"details": {
"state": "CLOSED",
"failureRate": "10.0%",
"slowCallRate": "0.0%",
"bufferedCalls": 10,
"failedCalls": 1,
"notPermittedCalls": 0
}
}
}
}
}
}
Set up an alert for every transition to OPEN. An open circuit means a dependency is in trouble, and someone should know about it right away.
10. Testing a Circuit Breaker
Test that the fallback really works, by forcing the circuit open instead of waiting for real failures:
@SpringBootTest
class PaymentClientCircuitBreakerTest {
@Autowired
private PaymentClient paymentClient;
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
@AfterEach
void resetCircuit() {
circuitBreakerRegistry.circuitBreaker("paymentService").reset();
}
@Test
void returnsPendingResultWhenCircuitIsOpen() {
circuitBreakerRegistry.circuitBreaker("paymentService").transitionToOpenState();
PaymentResult result = paymentClient.charge(new PaymentRequest(1001L, new BigDecimal("49.99")));
assertThat(result.isPending()).isTrue();
}
}
To test the transitions themselves, point the client at a mock server, such as WireMock, that returns errors. Make a few calls and check that the state changes from CLOSED to OPEN. Small values for slidingWindowSize and waitDurationInOpenState in a test profile keep such tests fast.
11. Best Practices
- Use one circuit breaker per dependency, such as
paymentServiceandinventoryService, so one failing service does not block calls to healthy ones. - Count only technical failures. Put business errors such as “card declined” or “not found” in
ignoreExceptions. - Treat slow calls as failures with
slowCallDurationThreshold, because a slow service is often worse than a down one. - Always provide a meaningful fallback, even if it is just a clear error message.
- Combine with timeouts, so calls cannot hang without limit.
- Tune settings using real traffic data. A window of 10 calls suits a busy service, but may react too slowly for a rarely called one.
- Monitor and alert on state changes.
- Keep fallbacks simple and fast. A fallback that calls another unreliable service just moves the problem.
12. Common Mistakes to Avoid
| Mistake | What goes wrong | Fix |
|---|---|---|
| Fallback signature does not match | Startup or runtime error: fallback method not found | Same return type and parameters, plus an exception parameter last |
| Calling the protected method from the same class | The annotation is skipped, so there is no protection | Call it through another bean |
| Counting business errors as failures | The circuit opens although the service is healthy | Add them to ignoreExceptions |
Leaving minimumNumberOfCalls at 100 on a quiet service | The circuit never opens | Lower it to fit real traffic |
| Retrying while the circuit is open | Wasted time and extra delay | Ignore CallNotPermittedException in Retry |
| One shared breaker for all services | One failure blocks every call | One instance per dependency |
| No timeouts on the HTTP client | Calls hang far beyond the slow-call limit | Set connect and read timeouts |
13. Key Benefits
- Prevents cascading failures: a problem in one service stays in that service.
- Graceful degradation: users get a useful, if limited, response instead of an error page or a spinning wheel.
- Saves resources: failing fast frees threads, connections and memory that would otherwise wait on doomed calls.
- Helps the failing service recover: fewer incoming requests give it room to get back on its feet.
- Automatic recovery: the half-open state detects when the service is healthy again, with no manual action.
- Better visibility: circuit states and events show exactly which dependency is causing trouble.
14. Conclusion
Failures are normal in distributed systems; what matters is how your system reacts to them. The Circuit Breaker pattern watches calls to each dependency, opens when too many fail, answers instantly with a fallback while the dependency recovers, and closes again once trial calls succeed. In Spring Boot, Resilience4j makes this a matter of a dependency, a few lines of configuration and one annotation. Combine it with timeouts, retries and bulkheads, monitor it closely, and your services will bend under pressure instead of breaking.
Frequently Asked Questions (FAQ)
Q1. What is the difference between a circuit breaker and a retry?
| Retry | Circuit breaker | |
|---|---|---|
| Assumes the problem is | Brief, and gone in a moment | Lasting, and likely to continue |
| Reaction | Tries the same call again | Stops calling for a while |
| Risk if used alone | Floods a struggling service with even more requests | Gives up on calls that might have worked on a second try |
They complement each other. Retry handles a single dropped connection; the circuit breaker steps in when failures keep happening and stops the retries from making things worse.
Q2. How is a circuit breaker different from a timeout or a bulkhead?
- A timeout limits how long one call may wait. It does not remember previous calls.
- A bulkhead limits how many calls to one dependency can run at the same time, so it cannot use up all threads.
- A circuit breaker looks at recent history and stops calls entirely when a dependency keeps failing.
A timeout makes each failure quick, a bulkhead limits the damage, and the circuit breaker stops the repeated attempts. Production systems usually use all three together.
Q3. Should I use a COUNT_BASED or TIME_BASED sliding window?
COUNT_BASEDjudges the last N calls. It suits busy services with a steady flow of requests.TIME_BASEDjudges all calls from the last N seconds. It suits services with uneven traffic, because old results expire with time.
With a count-based window on a quiet service, a failure from an hour ago can still count against the service now. A time-based window avoids this:
slidingWindowType: TIME_BASED
slidingWindowSize: 60 # seconds
minimumNumberOfCalls: 10
Q4. How do I choose the failure threshold and window size?
Start from what normal looks like for that dependency, then pick values that only trigger on a real problem:
- Check the normal error rate in your metrics. If it is usually 1–2%, a 50% threshold leaves plenty of room for normal noise.
- Choose a window that sees enough calls to be meaningful, for example 20–100 calls for a busy service.
- Set
minimumNumberOfCallsso a handful of early failures cannot open the circuit. - Set
waitDurationInOpenStateto roughly how long the dependency usually needs to recover, often 10–60 seconds.
Then watch how the circuit behaves under real traffic and adjust. If it opens during normal operation, it is too sensitive; if it stays closed during outages, it is not sensitive enough.
Q5. Which HTTP errors should count as failures?
Count errors that show the remote service is unhealthy, and ignore errors caused by the request:
| Response | Count as failure? | Why |
|---|---|---|
| Connection refused, timeout | Yes | The service is unreachable or overloaded |
| 500, 502, 503, 504 | Yes | The service itself is failing |
| 429 Too Many Requests | Usually yes | The service is asking you to slow down |
| 400, 404, 409, 422 | No | Your request was wrong; the service is working fine |
| 401, 403 | No | A credentials problem, which an open circuit will not fix |
Use recordExceptions and ignoreExceptions, or a recordFailurePredicate for more complex rules.
Q6. If my service runs on five instances, do they share one circuit breaker?
No. Resilience4j keeps the circuit state in memory, so each instance has its own breaker and decides independently.
This is usually fine and even desirable. Every instance sees the failures quickly and opens its own circuit within a few calls. It also means a network problem affecting only one instance does not block the others.
The trade-off is that each instance sends its own trial calls during half-open state. With many instances, keep permittedNumberOfCallsInHalfOpenState small so a recovering service is not flooded.
Q7. Where should the circuit breaker live: in the caller or the called service?
In the caller. The breaker protects the service that makes the call: it stops that service from wasting threads on a dependency that is failing. The failing service cannot protect its callers, because it may be too overloaded to respond at all.
For example, the Order Service wraps its call to the Payment Service. If the Inventory Service also calls the Payment Service, it has its own circuit breaker for that call.
Q8. Can an API gateway or a service mesh provide circuit breaking instead of code?
Yes. Gateways such as Spring Cloud Gateway and service meshes such as Istio or Linkerd can apply circuit breaking to traffic between services without changing application code.
| In code (Resilience4j) | In gateway or mesh | |
|---|---|---|
| Custom fallbacks with business logic | Yes | Limited, usually just an error response |
| Code changes needed | Yes | No |
| Works for any language | Java only | Yes |
Many teams use both: the infrastructure provides a basic safety net for every service, and Resilience4j handles calls that need a smart fallback.
Q9. Is Netflix Hystrix still a good choice?
No. Hystrix has been in maintenance mode since 2018 and receives no new features. Spring Cloud also removed its Hystrix support.
Use Resilience4j for new projects. When migrating, the ideas map directly:
| Hystrix | Resilience4j |
|---|---|
@HystrixCommand(fallbackMethod = ...) | @CircuitBreaker(name = ..., fallbackMethod = ...) |
| Thread pool isolation | @Bulkhead |
| Command timeout | @TimeLimiter or HTTP client timeouts |
| Hystrix Dashboard | Actuator and Micrometer metrics in Grafana |
Q10. Should I use Spring Cloud CircuitBreaker or Resilience4j directly?
- Resilience4j directly (
resilience4j-spring-boot3) gives you annotations, YAML configuration and every feature of the library. It is the simplest choice for most Spring Boot services. - Spring Cloud CircuitBreaker is an abstraction layer with Resilience4j underneath. It offers a common programming API, and integrates with Spring Cloud Gateway and OpenFeign.
If you mainly protect method calls in your own code, use Resilience4j directly. If you use Spring Cloud Gateway or OpenFeign, the Spring Cloud starter connects them for you.
Q11. Why does my circuit breaker never open?
Check these common causes:
- Not enough calls. The failure rate is only calculated after
minimumNumberOfCallscalls. The default is 100. - The exception is caught inside the method, so the breaker only ever sees successes.
- The exception is not recorded, because it is missing from
recordExceptionsor listed inignoreExceptions. - The method is called from the same class, which skips the Spring proxy.
- The AOP starter is missing, so the annotation has no effect.
- The
namedoes not match the instance in your configuration, so the default settings are used.
GET /actuator/circuitbreakerevents shows every recorded call, which quickly reveals whether failures are being counted.
Q12. Why does my fallback method not get called?
The fallback signature must match exactly. Resilience4j looks for a method in the same class that has:
- The same return type as the protected method.
- The same parameters, in the same order.
- One extra exception parameter at the end, such as
Throwable,Exceptionor a more specific type.
public PaymentResult charge(PaymentRequest request) { ... }
// Correct
private PaymentResult paymentFallback(PaymentRequest request, Throwable ex) { ... }
// Wrong: return type differs, so it is never used
private String paymentFallback(PaymentRequest request, Throwable ex) { ... }
Also check the exception type. A fallback declared with CallNotPermittedException only runs while the circuit is open, not when the call itself fails.
Q13. Is it a good idea for a fallback to call another service?
Be careful. If the fallback depends on another remote service, that service can fail too, and you are back where you started. Good fallbacks are local and fast: a cache, a default value, or saving the work for later.
If you must call a backup service, for example a second payment provider, protect that call with its own circuit breaker and a short timeout:
private PaymentResult paymentFallback(PaymentRequest request, Throwable ex) {
return backupPaymentClient.charge(request); // has its own @CircuitBreaker
}
Q14. Can I open or close a circuit manually?
Yes, through the CircuitBreakerRegistry. This is useful during planned maintenance of a dependency, or to stop calls to a misbehaving partner immediately:
CircuitBreaker breaker = circuitBreakerRegistry.circuitBreaker("paymentService");
breaker.transitionToForcedOpenState(); // block all calls until told otherwise
breaker.transitionToDisabledState(); // let all calls through, no protection
breaker.transitionToClosedState(); // return to normal automatic behavior
Forced states do not change automatically, so remember to switch back. Expose such controls only through a secured admin endpoint.
Q15. Does Resilience4j work with WebClient and reactive code?
Yes. Add the resilience4j-reactor module, and the same annotation works on methods returning Mono or Flux:
@CircuitBreaker(name = "inventoryService", fallbackMethod = "cachedStock")
public Mono<StockLevel> getStock(String sku) {
return webClient.get()
.uri("/api/stock/{sku}", sku)
.retrieve()
.bodyToMono(StockLevel.class);
}
private Mono<StockLevel> cachedStock(String sku, Throwable ex) {
return Mono.justOrEmpty(stockCache.get(sku));
}
The fallback must return the same reactive type, Mono<StockLevel> here.
Q16. Can I use a circuit breaker with OpenFeign clients?
Yes. With Spring Cloud OpenFeign and the Spring Cloud CircuitBreaker Resilience4j starter, enable it with one property:
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
Then provide a fallback class that implements the Feign interface:
@FeignClient(name = "payment-service", fallback = PaymentClientFallback.class)
public interface PaymentFeignClient {
@PostMapping("/api/payments")
PaymentResult charge(@RequestBody PaymentRequest request);
}
@Component
class PaymentClientFallback implements PaymentFeignClient {
@Override
public PaymentResult charge(PaymentRequest request) {
return PaymentResult.pending(request.orderId());
}
}
Q17. What happens to extra calls while the circuit is half-open?
Only the configured number of trial calls (permittedNumberOfCallsInHalfOpenState) are let through. Any other calls arriving at the same time are rejected with CallNotPermittedException, exactly as in the open state, so they go to the fallback.
Once the trial calls finish, the breaker calculates their failure rate. Below the threshold, it closes and all calls flow again. At or above it, it opens for another full wait period.
Q18. How can I get notified when a circuit changes state?
Subscribe to the circuit breaker’s events. This example logs every state change and sends an alert when a circuit opens:
@Component
public class CircuitBreakerAlerts {
private static final Logger log = LoggerFactory.getLogger(CircuitBreakerAlerts.class);
public CircuitBreakerAlerts(CircuitBreakerRegistry registry, AlertService alertService) {
registry.getAllCircuitBreakers().forEach(breaker ->
breaker.getEventPublisher().onStateTransition(event -> {
log.warn("Circuit {} changed: {}", event.getCircuitBreakerName(), event.getStateTransition());
if (event.getStateTransition().getToState() == CircuitBreaker.State.OPEN) {
alertService.notifyOnCall("Circuit OPEN: " + event.getCircuitBreakerName());
}
}));
}
}
In larger setups, alert on the resilience4j.circuitbreaker.state metric in your monitoring system instead, so alerts work the same way across all services.
Q19. Do I need a circuit breaker around my own database?
Usually not for your primary database. If it is down, there is rarely a useful fallback, and the connection pool (HikariCP) already limits waiting through its own timeout settings.
Circuit breakers make sense around dependencies where a fallback does exist:
- A read-only reporting database or search engine, where you can show cached or partial results.
- A cache server such as Redis, where you can fall back to the main database.
- Third-party APIs, which you cannot control or fix.
Q20. What HTTP status should my API return when the circuit is open and there is no fallback data?
Return 503 Service Unavailable, ideally with a Retry-After header that tells clients when to try again. This is clearer than a generic 500 error, because it says the problem is temporary:
@RestControllerAdvice
public class ResilienceExceptionHandler {
@ExceptionHandler(CallNotPermittedException.class)
public ResponseEntity<ProblemDetail> handleOpenCircuit(CallNotPermittedException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.SERVICE_UNAVAILABLE,
"This feature is temporarily unavailable. Please try again shortly.");
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.header(HttpHeaders.RETRY_AFTER, "30")
.body(problem);
}
}
Avoid exposing internal details such as the name of the failing service in the response.