Fallbacks & Composition
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
A fallback answers when a protected call fails, and composition decides which patterns see that failure first. Both are
easy to get subtly wrong: a fallback in the wrong place hides failures from the pattern outside it, and an order nobody
chose changes what the circuit breaker counts. This page covers the exact rules that resilience4j-spring6 applies to
fallbackMethod, fallbacks for reactive and asynchronous return types, what a good fallback looks like, and how to
combine Retry, CircuitBreaker, RateLimiter, TimeLimiter and Bulkhead either explicitly with Decorators or
through the annotations and their aspect order. It was written against Resilience4j 2.4.0, Spring Boot 4.1.x, Spring
Framework 7.0.x and Java 21 and verified on 4 October 2026: every rule below was run, not only read. The fallback and
aspect classes live in resilience4j-spring6, which both resilience4j-spring-boot4 and resilience4j-spring-boot3
use, so everything here behaves identically on Spring Boot 3.5.x (the same tests were run there); only the starter
artifact and its Java package differ. The individual patterns are in
Circuit Breaker and
Retry; the annotation mechanics (proxies, self-invocation) are in
Annotations & Aspect Order.
The examples were run in a test project where the remote client is replaced by a stub that counts its invocations and throws
whatever the test asks for; the snippets show the Bookshelf client (pricing.fetch(isbn)) where the stub stands, and nothing else
was changed.
In the running Bookshelf scenario the price of a book comes from the slow and flaky pricing-service. The call is
protected by a circuit breaker and a retry, and when the price cannot be fetched the page shows the last known price,
marked as stale, instead of an error.
For a rate limiter whose state lives in a shared store, the fallback question becomes whether to fail open or fail closed when that store is down; that choice is covered for Bucket4j in Resilience4j versus Bucket4j.
Fallback semantics
The fallback mechanism works like a try/catch block around one protected call. When a fallbackMethod is configured,
every exception thrown inside that annotation’s execution is handed to a fallback executor, which looks for the method
that best matches the exception, much as a catch clause is chosen. Three consequences follow from the source
(FallbackExecutor and DefaultFallbackDecorator) and were confirmed by running them:
-
The breaker state does not matter. An exception thrown by your method and a
CallNotPermittedExceptionthrown by an open circuit breaker take the same path: both reach the fallback. After the breaker has opened, the fallback answers without the remote call being made (in the run, the backend invocation count did not change while the circuit was open). -
Failures are recorded first. The fallback runs outside the pattern’s own execution, so the circuit breaker still records the failure that triggered it. A fallback hides the failure from the caller, not from the metrics.
-
Everything is caught, including exceptions the pattern ignores. An exception listed under the breaker’s
ignoreExceptionsis neither a success nor a failure for the breaker (in the run, the window stayed empty), but it still reaches the fallback. With a fallback parameter typedThrowable, even anErrorsuch asAssertionErroris caught; withException-typed parameters anErrorpasses through untouched.
@Service
public class PricingService {
private final PricingClient pricing;
public PricingService(PricingClient pricing) { this.pricing = pricing; }
@CircuitBreaker(name = "pricing", fallbackMethod = "lastKnownPrice")
public Money price(String isbn) {
return pricing.fetch(isbn);
}
// Same class, same parameters as price(String), plus one trailing exception parameter.
private Money lastKnownPrice(String isbn, CallNotPermittedException e) {
return new Money(isbn, 1999, true); // breaker open: serve the cached price
}
private Money lastKnownPrice(String isbn, IOException e) {
return new Money(isbn, 1999, true); // I/O problem: same degraded answer
}
private Money lastKnownPrice(String isbn, Exception e) {
return new Money(isbn, 0, true); // anything else: no price known, flagged stale
}
}
Source: Resilience4j: Spring Boot 2 & 3 (the "Fallback methods"
section) and the
FallbackExecutor
source at v2.4.0. Run on Resilience4j 2.4.0: a healthy call returns the live price, an IOException returns the cached
price, any other exception returns the unknown price, and once the breaker is open CallNotPermittedException selects the
first method.
All six annotations (@CircuitBreaker, @Retry, @RateLimiter, @Bulkhead, @TimeLimiter and @Timer) have a
fallbackMethod attribute, and each one resolves it independently of the others. The attribute is also resolved by the
SpEL resolver, so a placeholder such as ${bookshelf.fallback.name} works (the run resolved it from a property). It is
the position of the annotation that carries the fallback that decides what the other patterns see; the composition section
below returns to it.
Method-resolution rules
FallbackMethod finds the candidates by reflection once per annotated method and target class and caches them. The Javadoc of the class and readme.io both describe the intent, "same signature plus an exception"; the source
is stricter and richer than either. The table lists each rule with the outcome observed when running it against
Resilience4j 2.4.0.
| Rule | What the source does | Observed result |
|---|---|---|
The fallback lives in the same class as the annotated method, or in one of its superclasses |
|
A private method and a method inherited from an abstract base class were both found. A fallback in an unrelated class is never searched (from the source). |
The name matches |
|
A placeholder was resolved from the environment. |
The parameters are those of the annotated method, in the same order, plus one trailing |
Each original parameter type is compared with |
A fallback taking |
A fallback with only the exception parameter matches any annotated method |
A single parameter that is a |
One |
The return type is assignable to the original return type |
|
A fallback returning |
The closest exception type wins |
The thrown exception’s class is looked up, then its superclass, and so on up to |
|
With several candidates only superclasses are searched |
Only |
From the source; not run. |
With a single candidate that does not match the exception, the original exception is rethrown |
|
A lone |
No matching method at all disables the fallback |
|
The original exception reached the caller; the only trace is a |
Two methods for the same exception type with different parameter lists is an error |
|
It was thrown on every call, even with a healthy backend and before the backend was called. |
An exception thrown by the fallback replaces the original |
|
The caller saw the fallback’s |
A non-private fallback is invoked on the proxy |
|
A package-private fallback annotated with |
Two practical lessons come out of the table. First, the signature rule is exact, so an almost-right fallback fails silently into a log warning: check the warning, and prefer testing the fallback with the breaker forced open (see Testing). Second, the ambiguity error is thrown while resolving, so a single mistaken overload breaks every call of the method, including the successful ones.
@CircuitBreaker(name = "closest", fallbackMethod = "fbClosest")
public String closest(String isbn) { return backend.call(); }
String fbClosest(String isbn, NumberFormatException e) { return "NumberFormatException"; }
String fbClosest(String isbn, IllegalArgumentException e) { return "IllegalArgumentException"; }
String fbClosest(String isbn, Exception e) { return "Exception"; }
String fbClosest(String isbn, CallNotPermittedException e) { return "CallNotPermittedException"; }
// one fallback for two methods with different parameters
@CircuitBreaker(name = "shared1", fallbackMethod = "fbShared")
public String shared1(String isbn) { return backend.call(); }
@CircuitBreaker(name = "shared2", fallbackMethod = "fbShared")
public String shared2(String isbn, int page) { return backend.call(); }
private String fbShared(Throwable t) { return "shared:" + t.getClass().getSimpleName(); }
Source: Resilience4j: Spring Boot 2 & 3 and the
FallbackMethod
source at v2.4.0.
A cross-cutting fallback for several classes does not exist: there is no application-wide fallback registry. Share one with a common superclass (the inherited-method case above), or move to the explicit style of the next sections.
Fallbacks for reactive and asynchronous types
The fallback executor picks a FallbackDecorator from the annotated method’s return type. resilience4j-spring6 ships
decorators for CompletionStage, for Reactor’s Mono and Flux (registered when Reactor is on the classpath) and for the
RxJava 2 and RxJava 3 types (registered when RxJava is on the classpath); any other return type uses the plain
try/catch decorator. In every case the fallback must return the same kind of type as the annotated method: a Mono
fallback for a Mono method, a CompletableFuture for a CompletableFuture method.
@CircuitBreaker(name = "mono", fallbackMethod = "monoFallback")
public Mono<Money> monoPrice(String isbn) {
return pricing.fetchReactive(isbn);
}
private Mono<Money> monoFallback(String isbn, CallNotPermittedException e) { return Mono.just(new Money(isbn, 1999, true)); }
private Mono<Money> monoFallback(String isbn, Exception e) { return Mono.just(new Money(isbn, 0, true)); }
@CircuitBreaker(name = "flux", fallbackMethod = "fluxFallback")
public Flux<String> reviews(String isbn) { return reviewsClient.stream(isbn); }
private Flux<String> fluxFallback(String isbn, Exception e) { return Flux.empty(); }
@CircuitBreaker(name = "future", fallbackMethod = "futureFallback")
public CompletableFuture<Money> futurePrice(String isbn) { return pricing.fetchAsync(isbn); }
private CompletableFuture<Money> futureFallback(String isbn, CallNotPermittedException e) {
return CompletableFuture.completedFuture(new Money(isbn, 1999, true));
}
private CompletableFuture<Money> futureFallback(String isbn, Exception e) {
// e is the cause of the failure, not the CompletionException that wraps it
return CompletableFuture.completedFuture(new Money(isbn, 0, true));
}
Source: Resilience4j: Spring Boot 2 & 3 and the
fallback
decorators at v2.4.0. Run on Resilience4j 2.4.0 with resilience4j-reactor on the classpath: each fallback answered when
the call failed and again, without a backend call, once the breaker was open.
The decorators differ in one detail that matters when a method fails before it returns its reactive or asynchronous value:
| Return type | Failure inside the returned value | Exception thrown while the method itself runs |
|---|---|---|
|
|
The fallback is not called. The decorator only wraps the value that the method returned, so a synchronous throw
escapes as the original exception (observed: |
|
The decorator unwraps |
The aspect turns the throw into a failed stage, so the fallback is called (observed: fallback answered). |
RxJava 2 and 3 types |
|
As for Reactor, wrapped value only (from the source) |
So with Reactor, keep the method free of work that can throw before it returns the Mono or Flux (build the pipeline
lazily with Mono.defer or Mono.fromSupplier), or catch the synchronous error yourself. Reactor and RxJava specifics,
including the operators that apply the patterns without annotations, are in
Reactive, Kotlin & RxJava; the programming model
behind Mono and Flux is in Reactive Programming.
What a good fallback is
A fallback is a promise that the caller gets something useful when the real answer is not available. Four kinds cover almost every case; pick by how much a wrong or missing answer costs.
| Kind | Use it when | Bookshelf example |
|---|---|---|
Cached value |
A slightly old answer is better than none |
The last known price, flagged |
Default |
The feature is optional and a neutral value is correct |
An empty list of recommendations. |
Degraded response |
Part of the answer is still available |
The average rating without the review texts. |
Fail fast |
There is no honest answer and a wrong one is worse than none |
A clear |
// cached value, or fail fast when there is nothing to serve
@CircuitBreaker(name = "pricing", fallbackMethod = "cachedPrice")
public Money price(String isbn) {
Money fresh = pricing.fetch(isbn);
lastKnown.put(isbn, fresh);
return fresh;
}
private Money cachedPrice(String isbn, Exception e) {
Money cached = lastKnown.get(isbn);
if (cached == null) {
throw new PriceUnavailableException(isbn, e); // fail fast: a clear, mappable error
}
return new Money(isbn, cached.cents(), true); // flagged as stale
}
// default for an optional feature
@CircuitBreaker(name = "recommendations", fallbackMethod = "noRecommendations")
public List<String> recommendations(String isbn) { return recommendationsClient.similar(isbn); }
private List<String> noRecommendations(String isbn, Exception e) { return List.of(); }
// degraded response: the rating without the review texts
@CircuitBreaker(name = "reviews", fallbackMethod = "ratingOnly")
public Reviews reviews(String isbn) { return reviewsClient.fetch(isbn); }
private Reviews ratingOnly(String isbn, Exception e) { return new Reviews(4.5, List.of(), true); }
Source: Resilience4j: Spring Boot 2 & 3. Run on Resilience4j 2.4.0: the unseen ISBN failed fast with the cause attached, a cached price came back flagged stale, and the optional features degraded to an empty list and a rating-only response.
Rules that keep a fallback from becoming the next outage:
-
Cheap and local. A fallback must not call the dependency that just failed, nor another remote service that can fail the same way. Cache reads, constants and in-memory state are safe; if the fallback itself throws, its exception replaces the original.
-
Honest. Mark degraded answers (
stale,degraded) so the caller and the user can tell. A fallback that returns a plausible but invented value (a price of zero shown as free) is worse than an error. -
Observable. A fallback hides a failure from the caller, so it must leave a trace: a log line with the cause, a counter, or a field in the response. A dashboard that only watches errors reports a healthy system while every request is degraded.
-
Not a retry. A fallback answers once. Repeating the call is the job of
Retry, and the order between them decides whether the retry ever sees a failure (see below). -
Safe to repeat. A fallback that writes (queues an order, charges a card) needs the same idempotency as a retry, because the caller may call again.
Composing patterns: Decorators or annotations
The patterns are independent; composing them means nesting them. Resilience4j offers two ways, and they differ in who chooses the order.
With Decorators you choose the order explicitly. Every with… call wraps everything before it, so the last one
added is the outermost. Decorators documents the result as Fallback(Retry(CircuitBreaker(Supplier))) for a chain that
adds the circuit breaker, then the retry, then the fallback.
Supplier<Money> guarded = Decorators.ofSupplier(call)
.withBulkhead(bulkhead) // innermost: holds a permit only while the call runs
.withRateLimiter(rateLimiter) // every attempt takes a permit
.withCircuitBreaker(circuitBreaker) // every attempt is recorded
.withRetry(retry) // repeats the whole inner stack
.withFallback(
Arrays.asList(IllegalStateException.class, CallNotPermittedException.class),
e -> new Money("x", 1999, true)) // outermost: answers after the retries are exhausted
.decorate();
Source: the Decorators
source at v2.4.0 and Registries,
Configuration & Decorators. Run on Resilience4j 2.4.0 against a call that always fails: three backend invocations, three
failures recorded by the breaker, then the fallback value.
Two details of the builder were found while running it. The TimeLimiter and the thread-pool bulkhead exist only on the
CompletionStage builder, so a synchronous chain has no time limiter. And the Javadoc of Decorators shows
ofSupplier(…).withFallback(CallNotPermittedException.class, e → …), which does not compile: DecorateSupplier has
the overloads for a function, a list of exception types, a result predicate and a BiFunction, but not a single
exception class (that overload exists on ofCallable, ofFunction and the checked builders). Use the list form with
ofSupplier.
With annotations, Spring chooses the order through aspect ordering. The aspects of the starter are ordinary Spring AOP
aspects, and each has a numeric order. Spring’s Ordered contract is that a lower value means higher precedence,
which for an @Around advice means the outer one. The defaults are chosen so that the annotations nest as
Retry(CircuitBreaker(RateLimiter(TimeLimiter(Bulkhead(function))))).
Each annotation’s fallbackMethod is not a layer of this onion. It is executed by the aspect of the annotation that
declares it, around that aspect’s own execution. Putting the fallback on @CircuitBreaker therefore places it inside
the Retry, and the retry then sees an answer instead of a failure:
// fallback on the INNER annotation: the retry never sees a failure
@Retry(name = "inner")
@CircuitBreaker(name = "inner", fallbackMethod = "degraded")
public Money fallbackOnBreaker(String isbn) { return pricing.fetch(isbn); }
// fallback on the OUTER annotation: the attempts are exhausted first
@Retry(name = "outer", fallbackMethod = "degraded")
@CircuitBreaker(name = "outer")
public Money fallbackOnRetry(String isbn) { return pricing.fetch(isbn); }
private Money degraded(String isbn, Exception e) { return new Money(isbn, 1999, true); }
Source: Resilience4j: Spring Boot 2 & 3 and the
CircuitBreakerAspect
source at v2.4.0. Run on Resilience4j 2.4.0 with a backend that always fails and maxAttempts: 3: the first method called
the backend once and returned the degraded value (the retry saw a success); the second called it three times and then
returned the degraded value.
The same rule decides the choice between the two styles. If the fallback must run after the retries, declare it on the
outermost annotation (the @Retry), or use Decorators, where withFallback goes last by construction.
The recommended order and why
The default order is also the recommended one: Retry outside CircuitBreaker, Bulkhead innermost, with the fallback
outside everything. Each position has a reason, and the measurements come from running the chains with Decorators and
the annotations.
| Position | Reason | Measured |
|---|---|---|
Retry outside the CircuitBreaker |
Every attempt is a separate decision for the breaker. When the breaker opens in the middle of a retry sequence, the next attempts are rejected without calling the backend. |
With the breaker window of 2 and |
CircuitBreaker outside the RateLimiter |
A rejection by your own limiter is then seen (and can be ignored) by the breaker, and an open breaker does not consume permits. |
See the |
RateLimiter inside the Retry |
The limiter protects the dependency, so every real attempt must take a permit. |
Retry outside: three permits for one failing request. Limiter outside the retry: one permit for the same three attempts. |
TimeLimiter inside the retry |
Each attempt gets its own deadline, and a timeout is a retryable failure. Only available for asynchronous return types. |
See Time Limiter. |
Bulkhead innermost |
The permit is held only while the call runs, not while the retry sleeps between attempts. |
Retry outside: the single permit was free during both backoff waits. Bulkhead outside the retry: the permit was held during both waits (0 free). |
Fallback outermost |
It answers only when everything inside has given up. |
Fallback outermost: three backend calls, then the fallback. Fallback inside the retry: one backend call. |
IntervalFunction probe = attempt -> {
availableDuringBackoff.add(bulkhead.getMetrics().getAvailableConcurrentCalls());
return 1L; // wait 1 ms
};
Retry retry = Retry.of("r", RetryConfig.custom().maxAttempts(3).intervalFunction(probe).build());
// Retry(Bulkhead(fn)): availableDuringBackoff == [1, 1]
Supplier<Money> bulkheadInside = Decorators.ofSupplier(call).withBulkhead(bulkhead).withRetry(retry).decorate();
// Bulkhead(Retry(fn)): availableDuringBackoff == [0, 0]
Supplier<Money> bulkheadOutside = Decorators.ofSupplier(call).withRetry(retry).withBulkhead(bulkhead).decorate();
Source: Resilience4j: Bulkhead and Resilience4j: Retry. Run on Resilience4j 2.4.0 with a bulkhead of one concurrent call, as printed in the comments.
Metrics inflation
The order also changes what the circuit breaker’s numbers mean. With the default order the breaker sits inside the retry, so every attempt is a recorded call: one failing request with three attempts adds three failures to the sliding window. With the order reversed, the breaker records one outcome per request, but the duration of that outcome includes the retry waits.
// Retry(CircuitBreaker(fn)): the default order
Supplier<Money> retryOutside = Decorators.ofSupplier(call)
.withCircuitBreaker(cb).withRetry(retry).decorate();
// -> backend invoked 3 times, breaker recorded 3 failed calls
// CircuitBreaker(Retry(fn)): reversed
Supplier<Money> breakerOutside = Decorators.ofSupplier(call)
.withRetry(retry).withCircuitBreaker(cb).decorate();
// -> backend invoked 3 times, breaker recorded 1 failed call
Source: Resilience4j: CircuitBreaker and the
CircuitBreaker
source at v2.4.0. The annotated form was run as well: @Retry plus @CircuitBreaker on one method with a failing
backend invoked it three times and the breaker recorded three ERROR events.
| Aspect | Retry(CircuitBreaker(fn)), the default |
CircuitBreaker(Retry(fn)), reversed |
|---|---|---|
Calls recorded per failing request |
One per attempt (3 with |
One |
Failure rate over the window |
Counts attempts: a window of 10 holds the failures of about three requests, so the breaker opens after fewer failing requests than the window suggests |
Counts requests, as the sizing of the window usually assumes |
Slow-call rate |
Each attempt is measured alone (0 slow calls with instant failures and a 30 ms backoff) |
The whole sequence is one call: the same instant failures plus the 30 ms backoff recorded 1 slow call with a 40 ms threshold |
Open breaker mid-sequence |
The remaining attempts are rejected at once, without a backend call |
The retry continues to call the backend until its attempts are used up; the breaker opens only after the request ends |
Retry waits |
Not part of the breaker’s timings |
Part of the breaker’s timings |
Neither order is wrong, but only the first one protects the backend while a request is still retrying, which is why it is
the default and the recommendation. The practical rule is to size minimumNumberOfCalls and slidingWindowSize for
attempts, not requests, when the retry is outside, and to read the breaker’s failureRate accordingly.
Retrying through an open breaker
The most instructive interaction is a retried call that meets a breaker which opens while the retry is still running.
Retry retries every exception by default, and CallNotPermittedException is an exception like any other, so the retry
keeps knocking on the closed door until its attempts are used up; the backend is not called, but each attempt costs a
wait. The run below used maxAttempts: 4, a breaker window of 2 and a fallback on the @Retry.
The run confirmed every step: the backend was invoked twice, the breaker rejected two attempts, the fallback received
the CallNotPermittedException and answered with the cached price, and a second request found the breaker still open and
made no backend call at all (all four of its attempts were rejected). Retrying a rejection is wasted time, so tell the
retry to stop:
resilience4j:
retry:
instances:
pricing:
maxAttempts: 4
waitDuration: 1ms
ignoreExceptions:
- io.github.resilience4j.circuitbreaker.CallNotPermittedException
Source: Resilience4j: Retry and Resilience4j: Spring Boot 2 & 3. Run on Resilience4j 2.4.0 with the annotations: the backend was invoked twice, only one attempt was rejected by the breaker, and the fallback answered immediately after it.
The AspectOrder properties
Each aspect reads its order from a property, so the global nesting can be changed without leaving the annotations. The
properties are plain integers; the defaults are expressed relative to Ordered.LOWEST_PRECEDENCE (2147483647), which is
why they look like large numbers.
| Aspect | Property | Default offset | Default value | Position in the default nesting |
|---|---|---|---|---|
|
|
|
|
Outermost |
|
|
|
|
Inside Retry |
|
|
|
|
Inside the breaker |
|
|
|
|
Inside the rate limiter |
|
|
|
|
Innermost |
|
|
|
|
Inside everything, nearest the method |
The values were read from the running aspect beans (getOrder() on RetryAspect, CircuitBreakerAspect,
RateLimiterAspect, TimeLimiterAspect and BulkheadAspect): -5, -4, -3, -2 and -1 relative to
LOWEST_PRECEDENCE. The bulkheadAspectOrder setter is new in 2.4.0, so that property can be set from YAML, which was not
possible before. The properties are global to the application: one order for every annotated method. To nest differently
for one call, use Decorators for that call.
The readme.io wording discrepancy
readme.io describes the properties this way: to make the circuit breaker start after the retry has finished its work, set
retryAspectOrder to a greater value than circuitBreakerAspectOrder (its words: "the higher value = the higher
priority"), and it gives an example with the breaker at 1 and the retry at 2. That is the opposite of what Spring does. In
Spring AOP a lower value is the higher precedence, hence the outer aspect, so the example makes the breaker the outer
aspect and the retry the inner one.
resilience4j:
circuitbreaker:
circuitBreakerAspectOrder: 1
retry:
retryAspectOrder: 2
Source: Resilience4j: Spring Boot 2 & 3 (the "Aspect order" section) and
the Spring Ordered
Javadoc. Run on Resilience4j 2.4.0: with these two properties the backend was still invoked three times but the breaker
recorded one failed call, which is the behaviour of CircuitBreaker(Retry(fn)).
To get the order the readme text intends (the retry outside the breaker) leave the defaults alone, or give the retry the lower number. Two cautions apply whenever you set these values:
-
The numbers are absolute. In the run above the other aspects kept
LOWEST_PRECEDENCE - 3,- 2and- 1, so setting the breaker to 1 and the retry to 2 moves both of them outside every aspect that keeps its default, including the rate limiter, the time limiter and the bulkhead. Set all the properties you rely on, or none. -
A smaller number also moves an aspect relative to every other aspect in the application, according to
Ordered. When the order matters for a mix of Resilience4j and other aspects, give every aspect an explicit order.
For the proxy mechanics behind this, see the Spring documentation of advice ordering and Annotations & Aspect Order.
Pitfalls
| Pitfall | Fix |
|---|---|
Fallback declared on the inner annotation |
The retry sees a success and never retries. Declare the fallback on the outermost annotation or use |
A fallback whose signature is almost right |
It is silently ignored with one |
Two overloads for the same exception type with different parameter lists |
|
A Mono method that throws before returning the Mono |
The fallback is skipped. Build the pipeline lazily or catch synchronous errors. |
A fallback that calls the failing dependency or another remote service |
It fails the same way and replaces the original exception. Use a cache, a default or a fail-fast exception. |
A fallback that invents data |
Mark degraded answers and prefer failing fast when no honest answer exists. |
Retry retrying |
Add it to the retry’s |
Reading the breaker’s failure rate per request |
With the default order it is per attempt: size the window and |
Copying the readme.io |
Lower value is outer. Verify the order with the metrics or by reading |
Setting one |
The numbers are absolute, so the aspect jumps outside every default-ordered aspect. |
Using |
It does not exist on that builder (the Javadoc example is wrong). Use the list overload or |
What changed recently
-
2.4.0 added the
bulkheadAspectOrdersetter, so the order of the bulkhead aspect can be set from YAML, and clarified the aspect-order defaults (Retry -5 to Bulkhead -1 relative toLOWEST_PRECEDENCE) in the starter. The release notes are in the GitHub release;RELEASENOTES.adocat the tag stops at 2.3.0. -
2.4.0 adds
resilience4j-spring-boot4. The fallback executor, the decorators and the aspects are the sameresilience4j-spring6classes as with Spring Boot 3, so fallbacks and ordering behave identically. -
The readme.io Spring Boot page still describes the aspect order as higher value meaning higher priority, with an example that produces the opposite nesting; the source and the run above follow Spring’s
Ordered. -
Spring Framework 7 resilience annotations (
@Retryable,@ConcurrencyLimit) have no fallback method; composing them with Resilience4j is covered in Spring Framework Native Resilience.
References
-
Resilience4j: Spring Boot 2 & 3 (fallback methods and aspect order)