When a downstream service gets slow, every request to it piles up waiting. Threads exhaust, your app stops responding, and one failing dependency becomes an outage. A circuit breaker fixes this by failing fast once errors cross a threshold. Here is how to add one with Resilience4j.
1. Add the Dependency
<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> <version>2.2.0</version> </dependency>
2. Configure the Breaker
In application.yml, define when the breaker trips and how it recovers:
resilience4j: circuitbreaker: instances: inventory: sliding-window-size: 10 failure-rate-threshold: 50 wait-duration-in-open-state: 10s permitted-number-of-calls-in-half-open-state: 3
This says: over the last 10 calls, if 50% fail, open the breaker for 10 seconds, then let 3 test calls through before deciding to close again.
3. Annotate the Call
Wrap the risky call and provide a fallback that runs when the breaker is open:
@Service public class InventoryClient { @CircuitBreaker(name = "inventory", fallbackMethod = "fallback") public Stock check(String sku) { return restClient.get() .uri("/stock/{sku}", sku) .retrieve() .body(Stock.class); } private Stock fallback(String sku, Throwable t) { return Stock.unknown(sku); // degrade gracefully } }
The fallback signature must match the original method plus a Throwable.
4. Understand the Three States
- โธClosed โ calls flow normally; failures are counted.
- โธOpen โ the failure threshold was crossed; calls fail instantly and hit the fallback without touching the downstream service. This is what saves your thread pool.
- โธHalf-open โ after the wait duration, a few probe calls decide whether to close (recovered) or re-open (still broken).
5. Add a Timeout Too
A circuit breaker counts failures, but a call that hangs forever never becomes a failure. Pair it with a time limiter so slow calls are counted:
resilience4j: timelimiter: instances: inventory: timeout-duration: 2s
Summary
A circuit breaker converts a slow, cascading failure into a fast, contained one. Configure a sliding window and failure threshold, annotate the risky call with a fallback, and always pair it with a timeout so hangs are caught. Your app stays responsive even when a dependency does not.