The Problem
When an external service becomes slow or unavailable, without a Circuit Breaker every incoming request to your service will wait for the timeout — blocking threads and degrading the entire application.
Your service → External service (slow) → Thread blocked for 30s
→ More requests arrive → Thread pool exhausted → General timeout
The Circuit Breaker breaks this cycle: after N consecutive failures, it "opens" and immediately rejects calls without waiting for the timeout.
Maven Dependency
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
Configuration (application.yml)
resilience4j:
circuitbreaker:
instances:
pagamentoService:
# Opens the circuit after 50% failure rate in a window of 10 calls
slidingWindowSize: 10
failureRateThreshold: 50
# Waits 10s before retrying (Half-Open state)
waitDurationInOpenState: 10s
# Number of test calls allowed in Half-Open state
permittedNumberOfCallsInHalfOpenState: 3
# Exceptions counted as failures
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.FeignException
Annotation-Based Implementation
@Service
public class PagamentoService {
@CircuitBreaker(name = "pagamentoService", fallbackMethod = "fallbackPagamento")
public PagamentoResponse processar(PagamentoRequest request) {
return adquirenteClient.processar(request);
}
// Fallback: returns a degraded response when the circuit is open
private PagamentoResponse fallbackPagamento(PagamentoRequest request, Exception ex) {
log.warn("Circuit open for acquirer. Reason: {}", ex.getMessage());
return PagamentoResponse.builder()
.status("PENDING")
.mensagem("Sistema temporariamente indisponível. Tente em instantes.")
.build();
}
}
Programmatic Implementation (more control)
@Service
public class PagamentoService {
private final CircuitBreaker circuitBreaker;
private final AdquirenteClient adquirenteClient;
public PagamentoService(CircuitBreakerRegistry registry, AdquirenteClient client) {
this.circuitBreaker = registry.circuitBreaker("pagamentoService");
this.adquirenteClient = client;
}
public PagamentoResponse processar(PagamentoRequest request) {
return circuitBreaker.executeSupplier(
() -> adquirenteClient.processar(request)
);
}
}
Circuit Breaker States
| State | Behavior | |-------|----------| | Closed | Normal — all calls pass through | | Open | Circuit open — immediately rejects calls, routes to fallback | | Half-Open | Probing — allows N test calls through |
Monitoring with Actuator
management:
endpoints:
web:
exposure:
include: health,circuitbreakers
health:
circuitbreakers:
enabled: true
Endpoint: GET /actuator/health displays the state of each circuit breaker.
Combining with Retry
Retry should be applied before the Circuit Breaker (innermost decorator):
@Retry(name = "pagamentoService", fallbackMethod = "fallbackPagamento")
@CircuitBreaker(name = "pagamentoService", fallbackMethod = "fallbackPagamento")
public PagamentoResponse processar(PagamentoRequest request) {
return adquirenteClient.processar(request);
}
Resilience4j applies the correct order automatically: Retry → CircuitBreaker → Bulkhead → TimeLimiter.
When NOT to Use a Circuit Breaker
- Calls to your own database (use connection pool limits instead)
- Idempotent, low-cost operations with cheap retries
- When the fallback creates data inconsistency (e.g., incorrect balance)