Circuit Breaker with Resilience4j in Spring Boot: Timeout, Retry, Fallback and Metrics
Where I use circuit breakers
External supplier, payment and downstream microservice calls can become slow long before they become completely unavailable.
If every request waits:
thread
connection
memory
request slotremain occupied.
The circuit breaker protects the caller by failing fast when evidence says the dependency is unhealthy.
State model
CLOSED
|
failure/slow-call threshold
v
OPEN
|
waitDuration
v
HALF_OPEN
|
controlled probe calls
+-- success -> CLOSED
`-- failure -> OPENResilience4j configuration
resilience4j:
circuitbreaker:
instances:
hotelSupplier:
slidingWindowType: COUNT_BASED
slidingWindowSize: 20
minimumNumberOfCalls: 10
failureRateThreshold: 50
slowCallRateThreshold: 50
slowCallDurationThreshold: 2s
waitDurationInOpenState: 20s
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
retry:
instances:
hotelSupplier:
maxAttempts: 2
waitDuration: 150ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
timelimiter:
instances:
hotelSupplier:
timeoutDuration: 2500msI treat a slow call as a failure signal because an HTTP 200 after twenty seconds can still destroy the upstream SLA.
Spring service
@Service
public class HotelAvailabilityService {
private final HotelSupplierClient client;
public HotelAvailabilityService(
HotelSupplierClient client
) {
this.client = client;
}
@CircuitBreaker(
name = "hotelSupplier",
fallbackMethod = "fallback"
)
@Retry(name = "hotelSupplier")
public HotelAvailability search(
HotelSearchRequest request
) {
return client.search(request);
}
public HotelAvailability fallback(
HotelSearchRequest request,
Throwable error
) {
return HotelAvailability.degraded(
request.hotelId(),
"LIVE_AVAILABILITY_UNAVAILABLE"
);
}
}A fallback must be business-valid
For hotel search, returning a degraded state or cached non-bookable content can be acceptable.
For payment capture, this would be dangerous:
payment failed
fallback -> "SUCCESS"A fallback is not a mechanism for hiding failures.
Possible fallback choices:
- cached result;
- partial data;
- queue for later;
- alternate provider;
- explicit unavailable response.
Exception classification
Business errors such as:
invalid date
supplier rejects unsupported room type
authentication deniedshould not necessarily count against circuit health.
I configure recorded/ignored exceptions so the breaker represents dependency health rather than user mistakes.
Retry and circuit breaker ordering
I do not want:
one call
-> retry 5 times
-> breaker sees one final failurewithout understanding the load effect.
Nor do I want a breaker to count every local validation failure.
Decorator order and metrics need to be tested under load.
My usual sequence is conceptually:
overall deadline
|
bulkhead/concurrency
|
circuit breaker
|
small retry policy
|
HTTP timeoutThe exact Resilience4j annotation/decorator order depends on implementation, so I verify it with metrics rather than assuming.
Metrics
I expose:
state
failure rate
slow-call rate
buffered calls
not-permitted calls
successful/failed calls
retry attempts
timeout count
fallback countPrometheus examples usually come from Resilience4j Micrometer integration.
Operationally, not_permitted_calls is important: it tells me the breaker is actively shedding requests.
Half-open traffic
A half-open state should permit only a few test calls.
If 500 pods all send unrestricted probes at the same time, the recovering dependency can be knocked over again.
Failure scenarios
Breaker opens too aggressively
Increase minimum calls/window based on traffic shape. A service receiving two calls per hour needs a different policy from 10,000 calls/sec.
Fallback returns stale sensitive data
Do not cache/fallback where freshness is legally/business-critical.
Breaker never opens on slow calls
Track slow-call rate, not only exceptions.
Every instance has local breaker state
That is normally acceptable. Sharing breaker state centrally introduces another dependency. The load balancer spreads observations across instances, so thresholds should be tested at instance level.
Production checklist
- Timeout first.
- Count slow calls.
- Ignore business/validation errors.
- Keep retry attempts low.
- Use jitter for retry delays.
- Define valid fallback semantics.
- Export breaker metrics.
- Alert on OPEN duration and rejected calls.
- Test HALF_OPEN under load.
- Pair with bulkhead/concurrency control.
FAQ
Does circuit breaker replace timeout?
No. Timeout bounds one request. Circuit breaker uses history to stop sending requests.
Does it replace retry?
No. Retry handles a few transient failures. Circuit breaker protects against persistent failure.
Should every HTTP call have a breaker?
No. Use it where dependency failure can create cascading resource pressure or fast failure has business value.
Related architecture guides
- [Microservices Design Patterns I Have Used in Real Enterprise Projects](/articles/microservices-design-patterns-real-enterprise-projects)
- [Circuit Breaker in Node/NestJS with Opossum](/articles/circuit-breaker-nodejs-nestjs-opossum)
-
Architect's final take
The value of Resilience4j is not the annotation. The value is making timeout, retry, slow-call behavior and fallback measurable and deliberately bounded.
Your feedback helps prioritize deeper technical content.






