The three-state machine in production via Resilience4j: failure-rate thresholds and sliding windows, fallback design, which failures should trip the breaker, and Resilience4j vs the deprecated Hystrix.
Published September 23, 2026
Design a Circuit Breaker built this exact three-state machine from scratch. This lesson is the production version — Resilience4j — and the operational decisions around it.
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackCharge")
public ChargeResult chargeCard(ChargeRequest request) {
return paymentClient.charge(request);
}
public ChargeResult fallbackCharge(ChargeRequest request, Exception ex) {
return ChargeResult.pending("Payment service unavailable, queued for retry");
}
Closed (normal): calls pass through, failures are tracked. Open (fast-fail): calls are rejected immediately, routed straight to the fallback, without even attempting the real call — protecting a struggling dependency from additional load while it recovers. Half-open (trial): after a configured wait duration, a limited number of trial calls are let through to test recovery — matching the from-scratch implementation in Design a Circuit Breaker exactly.
resilience4j:
circuitbreaker:
instances:
paymentService:
sliding-window-size: 20 # consider the last 20 calls
failure-rate-threshold: 50 # trip if >= 50% of those 20 failed
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 5
This is the meaningful upgrade over a simple cumulative failure counter (as Design a Circuit Breaker's hand-rolled version used): a sliding window considers only the most recent N calls, so failures from an hour ago don't still count against a service that's been healthy since — an old failure eventually "ages out" of the window automatically, giving a much more accurate signal of current health than an ever-accumulating count that only resets on a full success.
Three common fallback shapes, in rough order of preference when available: default value (return a safe, generic response — a recommendation service's fallback might be "no recommendations" rather than an error), cached value (return the last known-good result, even if slightly stale — appropriate when staleness beats absence), graceful degradation message (explicitly tell the caller this feature is temporarily unavailable, rather than either crashing or silently returning wrong-looking data). Choosing the wrong shape is a real failure mode itself — returning a default value that looks like a legitimate result (rather than being clearly marked as a fallback) can mislead calling code or users into trusting degraded data as if it were normal.
@CircuitBreaker(name = "paymentService")
// Resilience4j default: counts exceptions as failures, but this is configurable —
// record-exceptions / ignore-exceptions lets you exclude specific types
Worth tripping the breaker: 5xx server errors, timeouts — signals the dependency itself is unhealthy. Should NOT trip the breaker: 4xx client errors (a 400 Bad Request means the caller sent something wrong, not that the dependency is struggling) — counting client errors toward the failure threshold would trip the breaker due to caller mistakes, incorrectly treating a healthy-but-misused dependency as failing. This distinction — worth stating explicitly and unprompted — is exactly the kind of nuance that separates "knows the pattern exists" from "has actually operated one in production."
circuitBreakerRegistry.circuitBreaker("paymentService").getEventPublisher()
.onStateTransition(event -> log.warn("Circuit breaker state: {}", event));
A circuit breaker tripping to OPEN is itself a significant operational signal — it means a real dependency is failing badly enough to warrant fast-failing rather than attempting calls. Exposing state transitions to monitoring dashboards (see Metrics & Monitoring) means an OPEN breaker shows up as a visible, alertable event, not a silent internal detail only discoverable by noticing degraded functionality downstream.
Hystrix (Netflix's original circuit breaker library) is deprecated and no longer actively maintained — it used thread-pool isolation (a dedicated thread pool per protected call, adding real overhead per call and consuming meaningfully more resources). Resilience4j is lightweight and functional (composable decorators around a Supplier/Function, no dedicated thread pool required by default) — the modern default choice for any new Spring Boot service, with Hystrix knowledge mainly relevant for understanding or maintaining legacy codebases still running it.
Q: Why would thread-pool isolation (Hystrix's approach) ever have been preferable despite its overhead? A: It provided true bulkhead isolation as a side effect — a slow call literally couldn't consume threads beyond its dedicated pool, protecting the rest of the application even under pathological blocking. Resilience4j's lighter functional approach doesn't provide this isolation automatically, which is exactly why Bulkhead & Rate Limiting's separate bulkhead pattern exists as its own explicit tool.
Q: Can a circuit breaker and a retry mechanism be combined, and in what order? A: Yes, and the ordering matters — retries should generally happen INSIDE a closed breaker (retrying while the breaker still considers the dependency healthy), and retrying should stop the moment the breaker opens, exactly the combination covered in Retry & Backoff Strategies — retrying against an already-open breaker would defeat the breaker's whole purpose of stopping load to a struggling dependency.
Q: How would you choose sliding-window-size and failure-rate-threshold for a new integration? A: There's no universal correct value — a smaller window reacts faster to a real outage but is more sensitive to brief blips; start with a reasonable default (the 20-calls/50% shown above is a common starting point) and tune based on the dependency's actual observed failure patterns and how costly a false-positive trip is for that specific integration.
Q: What happens if the fallback method itself depends on something that's also failing? A: This is why fallback design explicitly favors the simplest, most reliable options (a static default, a value already in local memory) — a fallback with its OWN external dependency reintroduces the exact fragility the circuit breaker exists to protect against, defeating the pattern's purpose.