Files
spring-boot-demo/resilience4j-circuit-breaker/docs/02-circuit-breaker.md
T
asmhatre 320733265f Add resilience4j-circuit-breaker: Resilience4j 2.4.0 vs Spring Framework 7 core, on Boot 4.1
Companion module for the rewritten post 'Resilience4j Circuit Breaker in Spring Boot 4.1:
What It's Still For', reworked around Framework 7 now shipping @Retryable/@ConcurrencyLimit
in core. Covers what's still Resilience4j's job (circuit breaker, rate limiter, bulkhead's
bounded wait, fallback methods, Actuator/Micrometer metrics), the off-by-one between
maxAttempts and maxRetries, and two Boot-4.1 build breaks: spring-boot-starter-aop no longer
exists (renamed to spring-boot-starter-aspectj, proven with Maven Central metadata and the
renamed starter's own POM -- see resilience/docs/08-starter-aop-renamed-to-starter-aspectj.md
in this same repo), and the resulting fix uses that renamed starter directly rather than
assembling spring-aop + aspectjweaver by hand. Kept as its own module rather than a new
top-level repository, alongside the existing resilience/ module for the sibling Framework-7
post.
2026-09-18 08:40:50 +00:00

3.4 KiB

2. The circuit breaker, verified

Previous: 01-two-resilience-stacks.md | README | Next: 03-spring-retryable.md

Source: R4jPaymentService.java. Config: application.yml. Test: CircuitBreakerTripAndRecoverTest.java. Transcript: docs/output/01-circuitbreaker-trip.txt.

The state machine

CLOSED --(failure rate >= threshold, over >= minimumNumberOfCalls)--> OPEN
OPEN --(waitDurationInOpenState elapses)--> HALF_OPEN
HALF_OPEN --(permittedNumberOfCallsInHalfOpenState calls all succeed)--> CLOSED
HALF_OPEN --(a probe call fails)--> OPEN

This repo's config: slidingWindowSize=10, minimumNumberOfCalls=5, failureRateThreshold=50, waitDurationInOpenState=2s, permittedNumberOfCallsInHalfOpenState=2.

What the transcript actually shows

Calls 1-5 against a dead downstream: each one really calls the downstream, gets a DownstreamUnavailableException, and the fallback method runs. By call 5 the breaker has seen 5 calls (the configured minimum) at a 100% failure rate and flips to OPEN. Call 6 is the first call that does not touch the downstream at all — it gets CallNotPermittedException instead, straight from the breaker, before R4jPaymentService.pay()'s body ever runs.

Calls 7-9 (phase 2 in the transcript) confirm the downstream call counter is frozen at 5 for the rest of the OPEN period — three more calls, zero more downstream traffic. This is the property @Retryable cannot offer on its own (see chapter 3): a circuit breaker remembers that the dependency is down and stops asking.

After waitDurationInOpenState (2s; the test waits 2.2s to be safely past it) the breaker allows exactly permittedNumberOfCallsInHalfOpenState probe calls through. The downstream was reconfigured to succeed by then, both probes pass, and the breaker closes.

minimumNumberOfCalls and why it exists

A fresh breaker with one failed call and no minimumNumberOfCalls floor would trip on pure noise — one deployment-time connection refused, one cold JVM. minimumNumberOfCalls=5 here means the first four failures cannot trip anything by themselves; the breaker needs a real sample before it judges the dependency.

Actuator health vs the circuitbreakers endpoint

With management.endpoint.health.show-details: always, GET /actuator/health reports only an aggregate "circuitBreakers":{"status":"UNKNOWN"} in this version — no per-instance detail. The full breakdown (state, failureRate, bufferedCalls, notPermittedCalls, ...) is at GET /actuator/circuitbreakers instead. Both captured live, breaker actually OPEN, via ActuatorHealthTest.java: 05-actuator-health-tripped.txt.

  • Resilience4j reference: CircuitBreaker (rel=nofollow)
  • The actual states and transition rules, from the CircuitBreaker interface itself, are worth reading directly rather than from a diagram: io.github.resilience4j.circuitbreaker.CircuitBreaker.State.

Next: 03-spring-retryable.md.