What is a fallback method in a Resilience4j circuit breaker, and why do you need one?
answer
- fallbackMethod on @CircuitBreaker
- same bean, same return type, +trailing Throwable
- catches CallNotPermittedException when OPEN
- return cached / empty / degraded DTO
- self-invocation bypasses the aspect
basics
~20 sA fallback is an alternative method that runs when the protected call fails or the circuit is open. Instead of throwing an error to the caller, it returns a safe default (cached, empty, or degraded) response so the app keeps working.
solid answer
~40 sIn Resilience4j you annotate a method with `@CircuitBreaker(name="svc", fallbackMethod="fallback")`. When the wrapped call throws, times out, or the breaker is open (short-circuiting), Resilience4j invokes the fallback instead of propagating the exception. The fallback must live in the same bean, return the same type, and take the original arguments plus a trailing `Throwable` (or a specific exception subtype). Its job is graceful degradation: return a cached value, an empty list, a default object, or a 'service unavailable' DTO so one failing dependency doesn't break the whole request. Without a fallback, a `CallNotPermittedException` (open circuit) or the original exception bubbles up to the user. Fallbacks turn hard failures into soft, controlled ones.
code
java · 23 lines@Service
public class InventoryClientService {
private final RestClient restClient;
public InventoryClientService(RestClient restClient) {
this.restClient = restClient;
}
@CircuitBreaker(name = "inventory", fallbackMethod = "stockFallback")
public Stock getStock(String sku) {
return restClient.get()
.uri("/inventory/{sku}", sku)
.retrieve()
.body(Stock.class);
}
// same return type, same args + trailing Throwable
private Stock stockFallback(String sku, Throwable t) {
// degraded response: unknown availability, flagged for the UI
return Stock.unavailable(sku);
}
}go deeper
Know it returns a safe default instead of throwing; annotation is @CircuitBreaker(fallbackMethod=...).
Know the signature rules (same bean, same return type, trailing Throwable) and that OPEN circuit yields CallNotPermittedException.
Discuss most-specific fallback selection, self-invocation pitfall, logging the swallowed cause, and degraded-DTO design.
Frame fallbacks as a graceful-degradation policy across the system, deciding per endpoint whether degrade vs fail-fast, and how fallbacks compose with retry/bulkhead/timelimiter.
## The problem When a service calls a remote dependency (another microservice, a DB, an API), that dependency can be slow or down. If every request just waits and then throws, callers pile up, threads exhaust, and the failure **cascades** upstream. A **fallback** is the recovery path: a piece of code that produces a substitute result so the caller gets *something* useful (or at least a controlled error) instead of a raw exception. ## Resilience4j fallback mechanics **Resilience4j** is the resilience library Spring Cloud uses (Hystrix is retired). Its Spring Boot starter (`resilience4j-spring-boot3`) provides annotations backed by an AOP aspect. You write: ```java @CircuitBreaker(name = "inventory", fallbackMethod = "inventoryFallback") public Stock getStock(String sku) { ... remote call ... } private Stock inventoryFallback(String sku, Throwable t) { return Stock.unknown(sku); } ``` The aspect wraps `getStock`. The fallback is invoked when: - the wrapped method **throws** an exception the breaker records as a failure, **or** - the **circuit is OPEN**, in which case the call is short-circuited and Resilience4j throws `CallNotPermittedException` — the fallback catches that too. ## Fallback method rules (critical) 1. **Same bean** — the fallback must be a method on the same Spring bean (the aspect resolves it by reflection on `this`). 2. **Same return type** as the original. 3. **Same parameters, plus a trailing exception parameter.** You can declare `Throwable`, or a specific type like `IOException`. Resilience4j picks the **most specific** matching fallback when you declare several overloads. 4. It must be a real method (any visibility); it does **not** need its own annotation. If no fallback signature matches the thrown exception, Resilience4j throws `NoSuchMethodException`-style failure and the original exception effectively propagates — a common gotcha. ## What a good fallback returns - **Cached/last-known-good** data (best UX). - **Empty or default** value (`emptyList()`, a zero-count DTO). - **A degraded DTO** with a flag like `degraded=true` so the UI can show 'live data unavailable'. - **A fast, controlled error** (e.g., throw a domain `ServiceUnavailableException` mapped to HTTP 503) when there's genuinely no safe substitute. ## Gotchas - Fallbacks run for **every** failure the breaker counts — make them cheap and side-effect-free; don't call another flaky remote inside a fallback without its own protection. - Annotation-based fallbacks only fire when the call goes **through the proxy** — an internal `this.getStock()` self-invocation bypasses the aspect (Spring AOP limitation), so no fallback runs. - The fallback swallows the exception; **log it** (or Resilience4j's failure metrics hide the root cause). - Combining with `@Retry`/`@Bulkhead`/`@TimeLimiter`: order matters (see follow-ups) — the fallback is the outermost recovery. ## When to use Any cross-service or cross-process call in a microservice architecture where a degraded answer beats a broken request. Non-critical/enrichment calls (recommendations, ratings) are ideal fallback candidates; for critical writes, failing fast is often safer than a silent fake success.
- What exception is thrown to the fallback when the circuit is OPEN rather than when the call itself fails?`io.github.resilience4j.circuitbreaker.CallNotPermittedException`. When OPEN, Resilience4j short-circuits without calling the remote and hands this exception to the fallback, so you can distinguish 'we're deliberately not calling it' from a real downstream error.
- Why might a fallback silently not fire even though the annotation is present?Two classic reasons: (1) internal self-invocation — the call didn't go through the Spring AOP proxy, so the aspect never wrapped it; (2) no fallback overload matches the thrown exception's type, so Resilience4j can't dispatch and the original exception propagates.