skip to content

Resilience4j Integration

Resilience4j supplies circuit breaker, retry, bulkhead, rate limiter and time limiter as annotations over a sliding window, with closed, open and half-open states. Interviewers ask you to explain the state machine and to justify your window and threshold numbers.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What does the @CircuitBreaker annotation do, and what are the three states of a Resilience4j circuit breaker?

level: juniorimportance: must knowfreq 78%

answer

  1. CLOSED -> OPEN -> HALF_OPEN cycle
  2. OPEN = fail fast, CallNotPermittedException
  3. sliding window records outcomes
  4. minimum-number-of-calls before evaluating
  5. fallbackMethod + trailing Throwable

basics

~20 s

@CircuitBreaker wraps a method so repeated failures 'trip' the breaker and stop further calls, failing fast instead of hammering a broken dependency. The three states are CLOSED (normal), OPEN (blocking calls), and HALF_OPEN (testing recovery).

solid answer

~40 s

@CircuitBreaker (from resilience4j-spring-boot) wraps a method and tracks its success/failure rate over a sliding window. In CLOSED state calls pass through and outcomes are recorded. When the failure rate crosses a configured threshold, it transitions to OPEN and short-circuits every call immediately (throwing CallNotPermittedException), giving the failing dependency time to recover. After a wait duration it moves to HALF_OPEN, permitting a limited number of trial calls; if they mostly succeed it returns to CLOSED, otherwise back to OPEN. You annotate the method with @CircuitBreaker(name = "backendA", fallbackMethod = "fallback") and configure thresholds under resilience4j.circuitbreaker in application.yml. The fallback method must share the signature plus a trailing Throwable parameter.

code

java · 26 lines
java
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class InventoryClient {

    private final RestClient restClient;

    public InventoryClient(RestClient restClient) {
        this.restClient = restClient;
    }

    @CircuitBreaker(name = "inventory", fallbackMethod = "fallbackStock")
    public int stockLevel(String sku) {
        return restClient.get()
                .uri("/stock/{sku}", sku)
                .retrieve()
                .body(Integer.class);
    }

    // Same signature + trailing Throwable. Called on failure OR when breaker is OPEN.
    private int fallbackStock(String sku, Throwable t) {
        return 0; // degrade gracefully
    }
}

go deeper

for a junior

Know the three states and that the annotation makes failing calls fail fast with an optional fallback.

for a middle

Explain failure-rate vs slow-call rate, minimum-number-of-calls, and the fallback method signature rule.

for a senior

Discuss sliding-window sizing, tuning wait-duration and half-open trials, and CallNotPermittedException handling in fallbacks.

for a principal

Reason about cascading-failure prevention across a service mesh, per-instance vs shared breaker registries, and observability via CircuitBreaker events/metrics.

**What it is.** Resilience4j is a lightweight fault-tolerance library that replaced Netflix Hystrix as the standard in the Spring ecosystem. With the `resilience4j-spring-boot3` starter, you get an AOP-based `@CircuitBreaker` annotation (from `io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker`) that you place on any Spring bean method. **The problem it solves.** If a downstream service (DB, REST API) is slow or down, naively retrying or blocking threads on it can exhaust your own thread pool and cascade the failure upstream. A circuit breaker detects the failing dependency and 'fails fast' — returning an error or fallback immediately instead of waiting on a doomed call. **The three main states:** - **CLOSED** — normal operation. Every call is allowed through and its outcome (success, exception, slow call) is recorded in a sliding window. This is the starting state. - **OPEN** — the breaker has 'tripped'. Calls are rejected instantly without invoking the protected method, throwing `CallNotPermittedException`. The breaker stays OPEN for a configured `wait-duration-in-open-state`. - **HALF_OPEN** — after the wait duration elapses, the breaker allows a limited number of trial calls (`permitted-number-of-calls-in-half-open-state`). If the failure rate of those trials is below the threshold it goes back to CLOSED; otherwise it returns to OPEN. **Two special states** you may also see: **DISABLED** (always allow, no metrics) and **FORCED_OPEN** (always reject) — set manually/programmatically, they never transition automatically. **What counts as a failure.** By default any exception thrown by the method counts. You can narrow this with `record-exceptions` / `ignore-exceptions`. There is also a separate **slow-call** dimension: calls exceeding `slow-call-duration-threshold` count toward `slow-call-rate-threshold`, which can trip the breaker even when nothing throws. **Key config (application.yml).** ```yaml resilience4j.circuitbreaker.instances.backendA: sliding-window-type: COUNT_BASED sliding-window-size: 10 failure-rate-threshold: 50 wait-duration-in-open-state: 10s permitted-number-of-calls-in-half-open-state: 3 minimum-number-of-calls: 5 ``` `minimum-number-of-calls` is a crucial gotcha: the failure rate is only evaluated after that many calls are recorded, so the breaker won't trip on the very first failure. **Fallback.** `fallbackMethod` names a method in the same class with the identical parameter list plus a trailing `Throwable` (or a more specific exception type). It's invoked both when the method fails and when the breaker is OPEN (`CallNotPermittedException`). **When to use.** Around remote calls to services that can fail or slow down. Don't wrap pure in-memory logic.

  • Why does the breaker not trip on the first failure even if failure-rate-threshold is 50%?
    Because of minimum-number-of-calls. The failure rate is only calculated once at least that many calls have been recorded in the sliding window; before that, the breaker stays CLOSED regardless of the rate.
  • What exception is thrown when a call is rejected in the OPEN state?
    CallNotPermittedException (a subclass of RuntimeException). If a fallbackMethod is defined it is invoked with this throwable instead of propagating.

saying these in an interview costs you the question

  • Saying the breaker trips immediately on the first error (ignores minimum-number-of-calls)
  • Confusing HALF_OPEN with OPEN — thinking HALF_OPEN blocks all calls
  • Claiming Resilience4j is the same as Hystrix / requires a Netflix dependency
  • Thinking only thrown exceptions trip the breaker (forgetting slow-call rate)

context

open as a page

How do you configure a Resilience4j instance in application.yml, and how do fallback methods and default/instance config interact?

level: middleimportance: should knowfreq 62%

basics

~20 s

You define named instances under resilience4j.<pattern>.instances.<name> in application.yml (thresholds, window, timeouts), optionally inheriting from a configs.default block. The annotation's name attribute must match the instance name; fallbackMethod names a same-signature method with a trailing Throwable.

open as a page

Explain the two @Bulkhead implementations in Resilience4j (semaphore vs thread-pool) and how @Bulkhead relates to @TimeLimiter.

level: seniorimportance: should knowfreq 48%

basics

~20 s

A bulkhead caps concurrent calls to protect resources. SEMAPHORE bulkhead limits concurrency on the caller's own thread; THREADPOOL bulkhead runs calls on a bounded, separate thread pool with a queue and returns a CompletableFuture. TimeLimiter caps how long an async (thread-pool) call may run.

open as a page

How does the circuit breaker's sliding window work — count-based vs time-based — and how are failure and slow-call rates evaluated?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The sliding window stores recent call outcomes. COUNT_BASED keeps the last N calls; TIME_BASED keeps calls from the last N seconds. The breaker computes failure-rate and slow-call-rate over the window and trips to OPEN when either crosses its threshold, but only after minimum-number-of-calls.

open as a page

When you stack @Retry, @CircuitBreaker, @RateLimiter, @TimeLimiter and @Bulkhead on one method, what is their execution order and why does it matter?

level: principalimportance: should knowfreq 33%

basics

~20 s

Resilience4j runs the aspects in a fixed nesting order (outer to inner): Retry -> CircuitBreaker -> RateLimiter -> TimeLimiter -> Bulkhead. Order matters because, e.g., Retry outside CircuitBreaker means retries are recorded by the breaker, and TimeLimiter must sit around the async call it limits.

open as a page