0Pricing
Spring Boot 4 Complete Guide · Lesson

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: 40

Modeling 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$BadRequest

Bulkhead 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-calls and max-wait-duration (how long a call waits for a permit before being rejected).
  • thread-pool-bulkhead: max-thread-pool-size, core-thread-pool-size, and queue-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: 20

Ordering, 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 Throwable to distinguish CallNotPermittedException (breaker open) from BulkheadFullException (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-exceptions so 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

  1. Context Propagation and Span Instrumentation
  2. Circuit Breakers and Bulkhead Isolation
  3. Rate Limiting, Retry, and Time Limiters
  4. Correlating Logs, Metrics, and Traces
← Back to Spring Boot 4 Complete Guide