How do you execute a protected call with a fallback using the CircuitBreaker abstraction, and when exactly is the fallback invoked?
answer
- create(id).run(supplier, fallback)
- Fallback fires on supplier throw OR circuit OPEN (CallNotPermittedException)
- Fallback receives the Throwable
- Single-arg run = exception propagates
- Don't call the sick dependency inside fallback
basics
~10 sGet a CircuitBreaker from the factory (factory.create("id")) and call run(supplier, fallback). The supplier holds the risky call; the fallback runs when that call throws or when the circuit is open, receiving the Throwable.
solid answer
~50 sInject a CircuitBreakerFactory, call create(id) to get a CircuitBreaker, then run the protected work: run(Supplier<T> toRun, Function<Throwable,T> fallback). The supplier is your risky call. The fallback is invoked when (a) the supplier throws an exception the breaker records as a failure, or (b) the circuit is already OPEN so the call is short-circuited with a CallNotPermittedException. In both cases the fallback receives the Throwable — the original exception, or the CallNotPermittedException — and returns a substitute value of the same type T. If you use the single-arg run(supplier), there is no fallback and the exception (or CallNotPermittedException) propagates to the caller. The fallback should be cheap and safe: it runs in the failure path, so it must not call the same failing dependency, and it should return quickly. The id passed to create determines which breaker instance (and thus which config and state) is used.
code
java · 27 linesimport io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
class InventoryClient {
private final CircuitBreakerFactory<?, ?> cbFactory;
private final RemoteInventory remote;
InventoryClient(CircuitBreakerFactory<?, ?> cbFactory, RemoteInventory remote) {
this.cbFactory = cbFactory;
this.remote = remote;
}
int stockFor(String sku) {
CircuitBreaker cb = cbFactory.create("inventory");
return cb.run(
() -> remote.lookup(sku), // protected call
throwable -> { // failure OR open-circuit path
if (throwable instanceof CallNotPermittedException) {
// circuit is OPEN — short-circuited, dependency not touched
return -1; // sentinel: 'unknown, degraded'
}
return 0; // genuine failure -> assume out of stock
}
);
}
}go deeper
Know the run(supplier, fallback) shape and that fallback returns a substitute on failure.
Enumerate both fallback triggers (thrown exception and open circuit) and that the Throwable is passed in.
Discuss id-based sharing of state, checked-exception wrapping, and fallback discipline.
Tie fallback semantics to TimeLimiter/timeout integration and reactive overloads, and to designing graceful degradation.
## The execution API The `CircuitBreaker` interface exposes two overloads: ```java <T> T run(Supplier<T> toRun); <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback); ``` - **`toRun`** — a `Supplier<T>` containing the call you want to protect. It is executed by the breaker so the breaker can observe success/failure/latency. - **`fallback`** — a `Function<Throwable, T>` producing an alternative result. It receives the `Throwable` that caused the fallback so you can branch on the failure type or log it. Both overloads are **blocking/imperative**. For reactive code there is a parallel `ReactiveCircuitBreaker.run(Mono<T>, Function<Throwable, Mono<T>>)` / `run(Flux<T>, ...)` obtained from a `ReactiveCircuitBreakerFactory`. ## Exactly when the fallback fires The two-arg `run` invokes the fallback in two situations: 1. **The supplier throws.** The breaker records the outcome as a failure (subject to any configured `ignoreExceptions`/`recordExceptions` predicate) and calls the fallback with that exception. 2. **The circuit is OPEN.** The supplier is never executed; the breaker throws (in Resilience4j) a `CallNotPermittedException`, and the fallback is invoked with *that* exception as its argument. If a **TimeLimiter** is configured and the call exceeds the limit, that surfaces as a timeout exception into the same failure path and triggers the fallback. ## What the fallback receives and must return The fallback's parameter is the `Throwable`, so you can distinguish, e.g., a `CallNotPermittedException` (circuit open) from a genuine `HttpServerErrorException` (dependency error) and respond differently. It must return a value assignable to `T` — the same type the supplier would have returned. A common mistake is returning `null` where callers assume non-null. ## Semantics without a fallback `run(supplier)` (single arg) has **no** fallback: any recorded exception or a `CallNotPermittedException` propagates to your caller. Use this when the caller already has its own error handling or when failing fast is the desired behavior. ## Gotchas - **Do not call the failing dependency inside the fallback** — you would re-enter the failure. Return cached/default/degraded data instead. - **Keep the fallback fast and side-effect-light** — it runs on the hot failure path. - **The fallback type must match** — mismatched generics are a frequent compile error; both supplier and fallback must produce `T`. - **Same id = shared breaker.** Calling `create("orders")` in two places yields breakers backed by the same configuration and state for that id, so failures aggregate. Use distinct ids to isolate call sites. - **Checked exceptions** — the `Supplier` cannot throw checked exceptions directly; wrap them (e.g., in a `RuntimeException`) inside the lambda if the protected call declares checked exceptions. - **Fallback exceptions** — if the fallback itself throws, that exception propagates to the caller; the breaker does not have a second fallback.
- What is the difference in behavior between run(supplier) and run(supplier, fallback) when the circuit is open?Both short-circuit without touching the dependency. run(supplier) lets the CallNotPermittedException propagate to the caller; run(supplier, fallback) catches it and invokes the fallback with that exception, returning a substitute value.
- Why should the fallback avoid calling the same downstream service?The fallback runs precisely because that downstream is failing or the circuit is open; calling it again re-enters the failure path, adds latency, and defeats the purpose of failing fast. Return cached or default data instead.
saying these in an interview costs you the question
- Claiming the fallback only runs on exceptions, forgetting the open-circuit CallNotPermittedException case
- Putting the same failing remote call inside the fallback
- Thinking run(supplier) somehow swallows exceptions silently