skip to content

How do you configure per-id and default circuit-breaker settings while keeping application code decoupled from Resilience4j?

level: seniorimportance: should knowfreq 40%

answer

  1. Customizer<Resilience4JCircuitBreakerFactory> bean
  2. configureDefault(id -> ...) vs configure(consumer, ids...)
  3. Resilience4JConfigBuilder + CircuitBreakerConfig + TimeLimiterConfig
  4. Call sites stay neutral; config quarantines Resilience4j types
  5. failureRateThreshold / slidingWindow / waitDurationInOpenState

basics

~10 s

Register a Customizer bean for the provider factory (e.g. Customizer<Resilience4JCircuitBreakerFactory>). In it, set a default config and per-id overrides via configureDefault/configure. Call sites still only use CircuitBreakerFactory.run, so they stay decoupled.

solid answer

~40 s

Configuration lives in a `Customizer` bean targeted at the concrete factory, keeping tuning out of call sites. For Resilience4j you declare a `@Bean Customizer<Resilience4JCircuitBreakerFactory>`. Inside, `configureDefault` sets the config applied to every breaker id, and `configure(consumer, "id1", "id2")` overrides specific ids. You build the config with `Resilience4JConfigBuilder`, plugging in a `CircuitBreakerConfig` (failure-rate threshold, sliding window, wait duration in open state, etc.) and optionally a `TimeLimiterConfig`. Because the configuration is a bean and the call sites depend only on the neutral `CircuitBreakerFactory` + `run`, business code never imports Resilience4j — you can retune or, in principle, swap providers without touching it. There is also a properties-based route (spring.cloud.circuitbreaker / resilience4j.circuitbreaker.* with enableGroupMeterFilter etc.), but the Customizer bean is the programmatic, provider-specific hook that the abstraction exposes for this exact separation of concerns.

code

java · 33 lines
java
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;

@Configuration
class ResilienceConfig {

    @Bean
    Customizer<Resilience4JCircuitBreakerFactory> defaults() {
        return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
            .circuitBreakerConfig(CircuitBreakerConfig.custom()
                .failureRateThreshold(50)
                .slidingWindowSize(10)
                .waitDurationInOpenState(Duration.ofSeconds(10))
                .build())
            .timeLimiterConfig(TimeLimiterConfig.custom()
                .timeoutDuration(Duration.ofSeconds(3)).build())
            .build());
    }

    @Bean
    Customizer<Resilience4JCircuitBreakerFactory> tightInventory() {
        return factory -> factory.configure(builder -> builder
            .circuitBreakerConfig(CircuitBreakerConfig.custom()
                .failureRateThreshold(25).build()),
            "inventory"); // overrides the default for id "inventory"
    }
}

go deeper

for a junior

Aware that configuration is separate from call sites; not expected to write a Customizer.

for a middle

Can name the Customizer bean and configureDefault vs configure.

for a senior

Writes the Customizer, distinguishes default vs per-id, and knows the CircuitBreakerConfig knobs plus TimeLimiter integration.

for a principal

Articulates the honest limit of decoupling (config is provider-bound) and weighs Customizer-beans vs properties for ops.

## Where configuration belongs The whole point of the abstraction is that **call sites carry no configuration** — they just do `factory.create(id).run(...)`. Configuration is injected out-of-band through a **`Customizer`** bean typed to the concrete provider factory. This is the one place where provider-specific types appear, isolated to a `@Configuration` class rather than smeared across services. ## The Resilience4j Customizer ```java @Bean Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() { return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) .circuitBreakerConfig(CircuitBreakerConfig.custom() .failureRateThreshold(50) .slidingWindowSize(10) .waitDurationInOpenState(Duration.ofSeconds(10)) .build()) .timeLimiterConfig(TimeLimiterConfig.custom() .timeoutDuration(Duration.ofSeconds(3)) .build()) .build()); } ``` Key pieces: - **`Resilience4JCircuitBreakerFactory`** — the concrete factory auto-configured by the Resilience4j starter (implements the neutral `CircuitBreakerFactory`). - **`configureDefault(Function<String, ...>)`** — supplies the config used for **every** id that has no specific override. The `String` is the breaker id. - **`Resilience4JConfigBuilder`** — Spring Cloud's builder that wraps a Resilience4j `CircuitBreakerConfig` and an optional `TimeLimiterConfig`. - **`CircuitBreakerConfig`** — the Resilience4j knobs: `failureRateThreshold` (percent of failures to open), `slidingWindowSize`/`slidingWindowType` (COUNT vs TIME based), `waitDurationInOpenState`, `permittedNumberOfCallsInHalfOpenState`, `slowCallRateThreshold`, `recordExceptions`/`ignoreExceptions`, etc. - **`TimeLimiterConfig`** — wraps calls with a timeout; a breach counts as a failure. ## Per-id overrides ```java @Bean Customizer<Resilience4JCircuitBreakerFactory> perId() { return factory -> factory.configure(builder -> builder .circuitBreakerConfig(CircuitBreakerConfig.custom() .failureRateThreshold(30).build()), "inventory", "pricing"); // ids these settings apply to } ``` `configure(Consumer<Resilience4JConfigBuilder>, String... ids)` overrides named ids. Multiple `Customizer` beans compose — you can have one default and several per-id customizers. You can add several beans; all are applied. ## Properties alternative Resilience4j also honors `application.yml` under `resilience4j.circuitbreaker.instances.<id>....` and Spring Cloud exposes `spring.cloud.circuitbreaker.resilience4j.*` toggles. Properties are convenient for ops tuning; the Customizer bean is better when config is dynamic or derived. Both coexist; the abstraction still keeps call sites clean either way. ## Decoupling caveat (the honest part) The **call sites** are genuinely provider-neutral. The **configuration** is not — a `Customizer<Resilience4JCircuitBreakerFactory>` names Resilience4j types. So swapping providers means rewriting the configuration beans, not the business code. That is an intentional trade-off: full neutrality on the hot path, provider-specific tuning quarantined in one place. ## Gotchas - **`configureDefault` runs lazily per id** — the function is invoked the first time an unknown id is created, so a `create(id)` with a never-configured id gets the default. - **Ordering** — a per-id `configure` for an id overrides the default for that id; the last matching customizer wins if two target the same id. - **Forgetting the TimeLimiter** — a breaker without a timeout still lets a slow call hang; slow-call detection needs `slowCallRateThreshold`/`slowCallDurationThreshold` or a `TimeLimiterConfig`. - **Group vs instance** — sharing config across ids via a 'group' is a Resilience4j concept; via the abstraction you approximate it by applying the same builder to multiple ids in one `configure` call.

  • If your call sites depend only on the abstraction, is your whole app really provider-independent?
    The call sites are, but the configuration is not. A Customizer<Resilience4JCircuitBreakerFactory> references Resilience4j types, so switching providers means rewriting the config beans (and the starter). Neutrality is on the execution path, not the tuning path — an intentional trade-off.
  • What config would you set to also protect against slow (not just failing) calls?
    Add a TimeLimiterConfig timeout so hung calls fail, and/or set slowCallDurationThreshold + slowCallRateThreshold on the CircuitBreakerConfig so a high rate of slow-but-successful calls also opens the breaker.

saying these in an interview costs you the question

  • Claiming the abstraction makes the entire app provider-agnostic including configuration
  • Putting Resilience4j config inline at every call site instead of in a Customizer bean
  • Assuming a circuit breaker alone bounds latency without a TimeLimiter or slow-call config

context