Circuit Breaker Pattern with Spring Cloud and Resilience4j
Master the Circuit Breaker pattern with Spring Cloud Resilience4j: CLOSED/OPEN/HALF_OPEN states, @CircuitBreaker, sliding windows, failure thresholds, and Actuator monitoring..
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Circuit breaker has three states: CLOSED (normal), OPEN (failing fast), HALF_OPEN (testing recovery)
- Annotate methods with @CircuitBreaker(name='cbName', fallbackMethod='fallbackMethod') from Spring Cloud CircuitBreaker
- Configure sliding window type (COUNT_BASED vs TIME_BASED), failure rate threshold, and slow call threshold in Resilience4j config
- Monitor state and metrics via /actuator/circuitbreakers and /actuator/circuitbreakerevents
- Listen to CircuitBreakerOnStateTransitionEvent for alerts and operational visibility
A circuit breaker works exactly like the electrical circuit breaker in your home. When too many 'faults' happen (too many failing service calls), the breaker trips OPEN and stops sending requests to the failing service — just like a tripped breaker stops electricity to protect your appliances. After a cooldown, it goes HALF_OPEN and tries a few test requests; if they succeed, it closes back to normal.
Distributed systems fail in ways that monoliths never do. A microservice calling an inventory service that's responding in 30 seconds instead of 300 milliseconds will exhaust its thread pool in minutes, causing requests to queue up, then cascade failures to every caller upstream. Without a circuit breaker, one slow service can take down an entire platform.
The circuit breaker pattern was popularized by Michael Nygard in 'Release It!' and formalized for JVM microservices by Netflix with Hystrix. Netflix decommissioned Hystrix in 2018, and the Spring Cloud ecosystem migrated to Resilience4j — a lightweight, modular fault-tolerance library that implements circuit breaking, rate limiting, bulkhead isolation, retry, and timeout as composable decorators.
The production pain point that drives adoption is almost always a cascading failure event. A downstream service degrades, API calls start timing out at 10 seconds instead of 200 milliseconds, and thread pools fill up in seconds. Without circuit breaking, callers retry their requests, which adds more load to the already-struggling downstream, creating a positive feedback loop of failure. A circuit breaker breaks this loop by failing fast for a configurable period.
Spring Cloud CircuitBreaker provides a unified abstraction over Resilience4j (and optionally Sentinel, Spring Retry) with Spring Boot auto-configuration. The @CircuitBreaker annotation on Spring beans integrates with Spring AOP to wrap method calls with circuit breaker logic transparently. The fallbackMethod receives the exception so your fallback logic can distinguish circuit-open scenarios from genuine business errors.
Sliding window configuration is where teams most often make mistakes. COUNT_BASED windows make decisions based on the last N calls, which works well for high-traffic services but reacts slowly on low-traffic services. TIME_BASED windows evaluate calls in the last N seconds, which is more appropriate for services with variable traffic patterns but requires enough requests per second to generate meaningful statistics.
This guide covers every aspect of the circuit breaker pattern as implemented in Spring Cloud with Resilience4j, from basic annotation usage to advanced event-driven alerting and Actuator-based monitoring in production.
Circuit Breaker State Machine: CLOSED, OPEN, HALF_OPEN
Understanding the Resilience4j state machine is prerequisite to correct configuration. The state machine has five states in Resilience4j: CLOSED, OPEN, HALF_OPEN, DISABLED, and FORCED_OPEN. The operational states are the first three.
CLOSED is normal operation. All calls pass through to the downstream service. Each call outcome is recorded in the sliding window. The failure rate and slow call rate are computed after minimum-number-of-calls have been recorded. If either rate exceeds its threshold, the circuit transitions to OPEN.
OPEN is the protective state. All calls immediately throw CallNotPermittedException without touching the downstream service. The fallback method (if configured) is called instead. The circuit remains OPEN for wait-duration-in-open-state (default 60s), then automatically transitions to HALF_OPEN.
HALF_OPEN is the probing state. A limited number of calls (permitted-number-of-calls-in-half-open-state) are allowed through to test if the downstream service has recovered. All other calls fail immediately (no waiting). After the permitted calls complete, if the failure rate is below the threshold, the circuit closes. If it's above, the circuit opens again and starts another wait period.
DISABLED and FORCED_OPEN are manually set states for operational control — useful for maintenance windows or chaos engineering. The state can be forced via the Actuator management endpoint or programmatically via CircuitBreakerRegistry.
State transition events are valuable for operational visibility. Register a CircuitBreakerEventPublisher listener or use Spring's @EventListener with CircuitBreakerOnStateTransitionEvent to fire alerts (PagerDuty, Slack) when circuits open. A circuit opening is a signal that requires investigation — either the downstream service is degraded or your circuit breaker thresholds are misconfigured.
@CircuitBreaker Annotation and Fallback Methods
The @CircuitBreaker annotation from Spring Cloud CircuitBreaker (io.github.resilience4j.spring.annotations) wraps the annotated method in a Resilience4j circuit breaker via Spring AOP. The name attribute must match a configured instance in resilience4j.circuitbreaker.instances. The fallbackMethod attribute specifies the name of a method in the same class that returns the same type.
Fallback method signatures must include all parameters of the original method plus a Throwable parameter as the last argument. The Throwable receives the exception that caused the fallback to trigger — this is critical for distinguishing between circuit-open scenarios (CallNotPermittedException) and actual downstream failures (FeignException, ConnectException). A fallback for a circuit-open scenario should return cached data; a fallback for a genuine 503 should return an appropriate error response or propagate the exception.
Multiple fallback methods can be chained for different exception types. If you define fallback methods with specific exception types as the last parameter (IOException fallback, TimeoutException fallback, Throwable fallback), Resilience4j selects the most specific matching method. This allows fine-grained fallback logic without a big switch statement in a single fallback method.
AOP limitations are the most common source of confusion: @CircuitBreaker only works when the call goes through a Spring proxy. Calling an annotated method from within the same class (this.protectedMethod()) bypasses the AOP proxy and the circuit breaker doesn't activate. The calling code must inject the Spring bean and call the method through the injected reference, or use self-injection.
COUNT_BASED vs TIME_BASED Sliding Windows
Resilience4j supports two sliding window algorithms: COUNT_BASED and TIME_BASED. Choosing the wrong one for your traffic pattern is one of the most common circuit breaker configuration mistakes.
COUNT_BASED (default) uses a circular array of the last N call outcomes. A sliding-window-size of 20 means the circuit breaker evaluates the most recent 20 calls. This is efficient (O(1) memory, O(1) computation per call) and reacts quickly to failure bursts. The limitation: on low-traffic services (5 calls per minute), 20 calls represents 4 minutes of history. A failure burst at minute 1 stays in the window until minute 5. On high-traffic services, 20 calls is milliseconds of history, potentially too reactive.
TIME_BASED divides time into N one-second epochs and maintains a circular array of epoch data. A sliding-window-size of 60 evaluates calls from the last 60 seconds. This provides consistent time-based semantics regardless of traffic volume. The limitation: on low-traffic services with 1 call per second, a 60-second window contains only 60 calls — sufficient for meaningful statistics. But if the service has 1 call per minute, a 60-second window rarely has enough data to compute a meaningful failure rate.
For high-traffic services (100+ RPS): use COUNT_BASED with window size 100-200. For medium-traffic services (1-100 RPS): either works; COUNT_BASED with size 50 is a safe default. For low-traffic services (<1 RPS): use TIME_BASED with a longer window (300 seconds) and increase minimum-number-of-calls to match expected volume. Services with highly variable traffic (batch jobs, event-driven) should use TIME_BASED.
The minimum-number-of-calls setting acts as a guard — the failure rate is only evaluated after this many calls have been recorded. Set it to at least 10-20 to avoid opening the circuit on a single burst of test failures.