A CircuitBreaker class built from scratch with closed/open/half-open transitions and a failure-threshold counter — mapped directly onto what Resilience4j actually does internally.
Published September 23, 2026
Circuit Breaker Pattern covers using Resilience4j's production-ready implementation. This lesson builds the same mechanism from scratch, connecting the concept to the library you'd actually reach for.
enum State { CLOSED, OPEN, HALF_OPEN }
class CircuitBreaker {
private State state = State.CLOSED;
private int failureCount = 0;
private final int failureThreshold;
private Instant openedAt;
private final Duration openDuration; // how long to stay OPEN before trying HALF_OPEN
synchronized <T> T execute(Supplier<T> call, Supplier<T> fallback) {
if (state == State.OPEN) {
if (Duration.between(openedAt, Instant.now()).compareTo(openDuration) >= 0) {
state = State.HALF_OPEN; // timed transition back to trial mode
} else {
return fallback.get(); // still open — fail fast, don't even attempt the call
}
}
try {
T result = call.get();
onSuccess();
return result;
} catch (Exception e) {
onFailure();
return fallback.get();
}
}
private void onSuccess() {
failureCount = 0;
state = State.CLOSED; // a successful HALF_OPEN trial call closes the breaker again
}
private void onFailure() {
failureCount++;
if (state == State.HALF_OPEN || failureCount >= failureThreshold) {
state = State.OPEN; // trip immediately on a failed trial call, OR once the threshold is crossed from CLOSED
openedAt = Instant.now();
}
}
}
failureCount crosses failureThreshold — the breaker gives up on the dependency and starts fast-failing every call via the fallback, without even attempting the real call.openDuration elapses, the next call is allowed through as a trial, to test whether the dependency has recovered.failureCount and fully closes the breaker; failure immediately reopens it (no second chance), resetting the open timer.This hand-rolled version is a deliberately simplified core of what Resilience4j's CircuitBreaker actually implements — the real library adds a sliding window (tracking failure rate over recent calls, not a simple cumulative counter, so an old failure eventually stops counting against the current state) and configurable wait duration in open state (matching openDuration here) — but the fundamental three-state machine and the execute()-wrapping-a-call-with-a-fallback shape is identical. Recognizing this mapping explicitly — "this is what CircuitBreaker.decorateSupplier() is actually doing internally" — is a stronger interview answer than treating the library as a black box.
Q: Why does a single failed HALF_OPEN trial call reopen the breaker immediately, rather than requiring several trial failures? A: HALF_OPEN exists specifically to probe cautiously — allowing only one (or a small, configurable number of) trial call(s) through avoids sending a burst of traffic back at a possibly-still-struggling dependency; immediately reopening on any trial failure is the conservative, safe default, trading a slightly slower recovery detection for not risking overwhelming a barely-recovering service.
Q: What's the actual difference between a simple failure COUNT threshold and Resilience4j's sliding window RATE? A: A raw count (as implemented above) never 'forgets' old failures within CLOSED state until a success resets it entirely — a sliding window instead considers only the last N calls (or calls within a time window), so a service that failed 5 times an hour ago but has succeeded consistently since doesn't still count those old failures toward tripping the breaker now, which is a meaningfully more accurate signal of CURRENT health.
Q: Should the fallback itself be allowed to fail? A: It should be designed to be extremely reliable and simple (a cached value, a static default, a graceful degradation message — see Circuit Breaker Pattern's fallback design options) precisely because it's the last line of defense; a fallback that can itself throw defeats the entire purpose of the breaker, which exists to guarantee SOME response rather than an unhandled exception.
Q: How would you unit test the OPEN → HALF_OPEN timed transition without actually waiting openDuration in a test? A: Inject a Clock (or a time-supplier abstraction) instead of calling Instant.now() directly, letting a test advance a fake clock instantly rather than sleeping — the same dependency-injection-for-testability principle applied to time-dependent logic throughout this course (e.g. Producer-Consumer Class Design's injectable Dice-equivalent reasoning).