Circuit Breakers and Bulkhead Isolation
Protect downstream calls with Resilience4j circuit breakers, bulkheads, and fallback methods.
Circuit Breakers and Bulkhead Isolation is a free Spring Boot 4 Complete Guide lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Spring Boot 4 Complete Guide learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Resilience Matters
In a distributed system, a single slow or failing downstream service can cascade into a total outage. If your service keeps calling a dead dependency, threads pile up waiting on timeouts until the whole application becomes unresponsive.
Resilience engineering is about containing failure. Two foundational patterns:
- Circuit Breaker — stop calling a failing dependency for a while, fail fast instead of waiting.
- Bulkhead — isolate resources so one saturated dependency cannot exhaust the threads or connections needed by the rest.
In Spring Boot 4 we implement these with Resilience4j, a lightweight, functional fault-tolerance library that replaced the now end-of-life Hystrix.
Adding Resilience4j
Spring Boot 4 integrates Resilience4j through the resilience4j-spring-boot3 starter (compatible with Boot 3.x/4.x) plus Spring AOP. The annotations are activated by an aspect that wraps your method calls.
You also pull in spring-boot-starter-aop and, for metrics, spring-boot-starter-actuator with Micrometer.
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>How a Circuit Breaker Works
A circuit breaker is a state machine that monitors the failure rate of calls:
- CLOSED — calls flow through normally. Failures are recorded in a sliding window.
- OPEN — once the failure rate crosses a threshold, the breaker trips. Further calls fail immediately (no downstream call) for a wait duration.
- HALF_OPEN — after the wait, a limited number of trial calls are permitted. If they succeed, it returns to CLOSED; if they fail, it returns to OPEN.
The key benefit: when a dependency is down, you fail fast instead of blocking threads on doomed calls, and you give the dependency room to recover.
Annotating a Protected Call
The @CircuitBreaker annotation wraps a method. The name ties it to a configuration instance, and fallbackMethod names a method to invoke when the call fails or the breaker is open.
The fallback must have the same signature plus a trailing Throwable (or a more specific exception) parameter.
@Service
public class PaymentClient {
private final RestClient restClient;
public PaymentClient(RestClient restClient) {
this.restClient = restClient;
}
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackCharge")
public ChargeResult charge(ChargeRequest request) {
return restClient.post()
.uri("/charges")
.body(request)
.retrieve()
.body(ChargeResult.class);
}
private ChargeResult fallbackCharge(ChargeRequest request, Throwable t) {
return ChargeResult.deferred(request.id(), "payment temporarily unavailable");
}
}Configuring the Breaker
Circuit breaker behavior is tuned in application.yml. You define default settings and per-instances overrides keyed by the name you used in the annotation.
sliding-window-type— COUNT_BASED or TIME_BASED.failure-rate-threshold— percent of failures to open the breaker.wait-duration-in-open-state— how long to stay OPEN before HALF_OPEN.permitted-number-of-calls-in-half-open-state— trial calls allowed.slow-call-duration-threshold/slow-call-rate-threshold— treat slow calls as failures.
resilience4j:
circuitbreaker:
configs:
default:
sliding-window-type: COUNT_BASED
sliding-window-size: 20
failure-rate-threshold: 50
slow-call-duration-threshold: 2s
slow-call-rate-threshold: 80
wait-duration-in-open-state: 10s
permitted-number-of-calls-in-half-open-state: 5
automatic-transition-from-open-to-half-open-enabled: true
instances:
paymentService:
base-config: default
failure-rate-threshold: 40Modeling the State Machine in Plain Java
To internalize the logic, here is a stripped-down, framework-free model of the CLOSED to OPEN transition using a count-based window. This is conceptually what Resilience4j does for you behind the annotation.
Run it to see the breaker trip once the failure rate crosses the threshold.
public class MiniBreaker {
enum State { CLOSED, OPEN }
static State state = State.CLOSED;
static int window = 10, threshold = 50;
static boolean[] results = new boolean[window];
static int idx = 0, count = 0;
static void record(boolean failure) {
results[idx] = failure;
idx = (idx + 1) % window;
if (count < window) count++;
int fails = 0;
for (int i = 0; i < count; i++) if (results[i]) fails++;
int rate = count == 0 ? 0 : (fails * 100 / count);
if (count == window && rate >= threshold) state = State.OPEN;
}
public static void main(String[] args) {
boolean[] calls = {false,true,false,true,true,false,true,true,false,true};
for (boolean fail : calls) {
record(fail);
System.out.println((fail ? "FAIL" : "OK ") + " -> state=" + state);
}
}
}Counting Records vs Ignoring Exceptions
Not every exception should trip the breaker. A 400 Bad Request means your input was wrong, not that the dependency is unhealthy — counting it as a failure would open the breaker for valid traffic.
Use record-exceptions to list throwables that count as failures, and ignore-exceptions for those that should pass through without affecting breaker state.
resilience4j:
circuitbreaker:
instances:
paymentService:
base-config: default
record-exceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- org.springframework.web.client.HttpServerErrorException
ignore-exceptions:
- com.example.payments.InvalidCardException
- org.springframework.web.client.HttpClientErrorException$BadRequestBulkhead Isolation
The bulkhead pattern (named after a ship's watertight compartments) limits how many concurrent calls a dependency can consume, so one slow service cannot drain the entire thread pool.
Resilience4j offers two flavors:
- SemaphoreBulkhead — caps concurrent calls on the caller's thread. Lightweight, no extra threads.
- ThreadPoolBulkhead — runs calls on a dedicated, bounded thread pool with a queue. Provides true isolation and only works with asynchronous returns (
CompletableFuture).
If the bulkhead is full, the call is rejected with BulkheadFullException, which your fallback can handle.
Applying a Bulkhead
The @Bulkhead annotation limits concurrency. With type = THREADPOOL the method must return a CompletableFuture and runs on the bulkhead's own pool. You can stack it with @CircuitBreaker — the annotations compose.
@Service
public class InventoryClient {
private final RestClient restClient;
public InventoryClient(RestClient restClient) {
this.restClient = restClient;
}
@CircuitBreaker(name = "inventory", fallbackMethod = "fallbackStock")
@Bulkhead(name = "inventory", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<StockLevel> getStock(String sku) {
return CompletableFuture.completedFuture(
restClient.get().uri("/stock/{sku}", sku)
.retrieve().body(StockLevel.class));
}
private CompletableFuture<StockLevel> fallbackStock(String sku, Throwable t) {
return CompletableFuture.completedFuture(StockLevel.unknown(sku));
}
}Configuring Bulkheads
Semaphore and thread-pool bulkheads have separate config sections.
bulkhead(semaphore):max-concurrent-callsandmax-wait-duration(how long a call waits for a permit before being rejected).thread-pool-bulkhead:max-thread-pool-size,core-thread-pool-size, andqueue-capacity.
Right-size these to the dependency's real capacity: a bulkhead larger than the downstream can handle defeats the purpose.
resilience4j:
bulkhead:
instances:
paymentService:
max-concurrent-calls: 25
max-wait-duration: 50ms
thread-pool-bulkhead:
instances:
inventory:
core-thread-pool-size: 8
max-thread-pool-size: 16
queue-capacity: 20Ordering, Fallbacks, and Observability
When multiple Resilience4j annotations decorate one method, they apply in a fixed aspect order (highest precedence first): Bulkhead → TimeLimiter → RateLimiter → CircuitBreaker → Retry. So Retry wraps the circuit breaker — a retried call that still fails counts toward the breaker.
Fallback design rules:
- Keep fallbacks fast and side-effect-free; never call the failing dependency again.
- Return degraded-but-valid data (cached value, default, queued-for-later).
- Inspect the
Throwableto distinguishCallNotPermittedException(breaker open) fromBulkheadFullException(overloaded).
Actuator exposes state and metrics at /actuator/circuitbreakers and via Micrometer (resilience4j_circuitbreaker_state), so you can alert on OPEN breakers.
private ChargeResult fallbackCharge(ChargeRequest request, CallNotPermittedException ex) {
return ChargeResult.deferred(request.id(), "breaker open");
}
private ChargeResult fallbackCharge(ChargeRequest request, BulkheadFullException ex) {
return ChargeResult.rejected(request.id(), "system busy");
}
private ChargeResult fallbackCharge(ChargeRequest request, Throwable t) {
return ChargeResult.deferred(request.id(), "payment unavailable");
}Quick Check
Test your understanding of bulkhead isolation.
Recap
You learned how to protect downstream calls in Spring Boot 4 with Resilience4j:
- Circuit breakers fail fast via the CLOSED → OPEN → HALF_OPEN state machine, tuned with sliding-window size, failure-rate and slow-call thresholds, and wait duration.
- Use
record-exceptions/ignore-exceptionsso client errors (4xx) don't trip the breaker. - Bulkheads cap concurrency — SemaphoreBulkhead on the caller thread, ThreadPoolBulkhead for true isolation with
CompletableFuture. - Fallback methods share the signature plus a trailing
Throwable; return degraded-but-valid data and never re-call the failing dependency. - Annotation order is Bulkhead → TimeLimiter → RateLimiter → CircuitBreaker → Retry; observe state through Actuator and Micrometer.
Together these patterns keep one failing dependency from taking down your whole service.
Frequently asked questions
Is the “Circuit Breakers and Bulkhead Isolation” lesson free?
Yes — the full text of “Circuit Breakers and Bulkhead Isolation” is free to read here on the web, and the Spring Boot 4 Complete Guide course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Spring Boot 4 Complete Guide course, upgrade to CoddyKit PRO.
What will I learn in “Circuit Breakers and Bulkhead Isolation”?
Protect downstream calls with Resilience4j circuit breakers, bulkheads, and fallback methods. You practise Spring Boot 4 Complete Guide with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Spring Boot 4 Complete Guide?
No prior experience is required. Spring Boot 4 Complete Guide on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Circuit Breakers and Bulkhead Isolation” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Spring Boot 4 Complete Guide lesson?
Yes. Every Spring Boot 4 Complete Guide lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Context Propagation and Span Instrumentation
- Circuit Breakers and Bulkhead Isolation
- Rate Limiting, Retry, and Time Limiters
- Correlating Logs, Metrics, and Traces