Files
spring-boot-demo/resilience4j-circuit-breaker/docs/01-two-resilience-stacks.md
T
asmhatre 320733265f Add resilience4j-circuit-breaker: Resilience4j 2.4.0 vs Spring Framework 7 core, on Boot 4.1
Companion module for the rewritten post 'Resilience4j Circuit Breaker in Spring Boot 4.1:
What It's Still For', reworked around Framework 7 now shipping @Retryable/@ConcurrencyLimit
in core. Covers what's still Resilience4j's job (circuit breaker, rate limiter, bulkhead's
bounded wait, fallback methods, Actuator/Micrometer metrics), the off-by-one between
maxAttempts and maxRetries, and two Boot-4.1 build breaks: spring-boot-starter-aop no longer
exists (renamed to spring-boot-starter-aspectj, proven with Maven Central metadata and the
renamed starter's own POM -- see resilience/docs/08-starter-aop-renamed-to-starter-aspectj.md
in this same repo), and the resulting fix uses that renamed starter directly rather than
assembling spring-aop + aspectjweaver by hand. Kept as its own module rather than a new
top-level repository, alongside the existing resilience/ module for the sibling Framework-7
post.
2026-09-18 08:40:50 +00:00

5.0 KiB

1. Two resilience stacks on one classpath

README | Next: 02-circuit-breaker.md

Spring Boot 4.1 sits on Spring Framework 7, and Framework 7 shipped something new under org.springframework.resilience: @Retryable, @ConcurrencyLimit, and the annotation that turns them on, @EnableResilientMethods. None of this existed in Framework 6. This repo pins down exactly what moved into core, what didn't, and two build breaks you will hit the moment you try to wire it up on Boot 4 — both found by running the build, not by reading a changelog.

What's actually in core (verified by javap, not by the reference docs)

org.springframework.resilience.annotation.Retryable (an annotation, not the unrelated org.springframework.core.retry.Retryable interface that also ships in spring-core — two classes with the same simple name in the same major version, easy to import the wrong one):

value(), includes(), excludes(), predicate(),
maxRetries(), maxRetriesString(),
timeout(), timeoutString(),
delay(), delayString(),
jitter(), jitterString(),
multiplier(), multiplierString(),
maxDelay(), maxDelayString(),
timeUnit()

org.springframework.resilience.annotation.ConcurrencyLimit:

value(), limit(), limitString(), policy()   // policy: BLOCK (default) or REJECT

Neither annotation has a fallbackMethod attribute. Retrying exhausted just rethrows. Rejecting under REJECT throws org.springframework.resilience.InvocationRejectedException (a java.util.concurrent.RejectedExecutionException subtype) straight at the caller.

There is no circuit breaker class anywhere in spring-context-7.0.9.jar (grepped the whole jar listing for circuitbreaker, ratelimit, bulkhead — zero matches). @ConcurrencyLimit is the closest core has to a Resilience4j Bulkhead, and even that is a decades-old class repurposed: ConcurrencyLimitBeanPostProcessor$ResilienceConcurrencyThrottleInterceptor extends org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor, which has shipped in Spring since the 1.x era.

@EnableResilientMethods is not automatic

Spring Boot 4.1's autoconfigure jar carries no auto-configuration for the resilience package — grepping spring-boot-autoconfigure-4.1.1.jar's listing for "resilien" returns nothing. You must put @EnableResilientMethods on a @Configuration class yourself (this repo puts it on the @SpringBootApplication class). Skip it and the annotations are inert: no error, no log line, the method just runs unprotected.

Build break #1: spring-boot-starter-aop no longer exists

Every pre-Boot-4 Resilience4j guide, including the version of this post it replaces, tells you to add spring-boot-starter-aop. On Boot 4 that dependency breaks the build outright:

[ERROR] 'dependencies.dependency.version' for org.springframework.boot:spring-boot-starter-aop:jar is missing.

repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-aop/maven-metadata.xml confirms it: the last version ever published is 4.0.0-M2, a milestone. It never reached Boot 4.0 GA and was not revived for 4.1. The fix is org.springframework:spring-aop directly — see pom.xml.

Build break #2 (silent): no aspectjweaver, no proxy, no error

Swapping in spring-aop alone gets you a clean build and a wrong result. Resilience4j's Spring integration (resilience4j-spring6, which is what resilience4j-spring-boot4 actually depends on — see below) implements CircuitBreakerAspect, BulkheadAspect, RateLimiterAspect and friends as real @Aspect classes (confirmed with unzip -l on the jar). Spring's AnnotationAwareAspectJAutoProxyCreator needs org.aspectj:aspectjweaver on the classpath to even recognise a bean as an aspect. Without it, zero proxies get created, for anything — Resilience4j's annotations and Spring's own @Retryable both go completely inert, silently.

That is exactly what happened building this repo: CircuitBreakerTripAndRecoverTest failed with the raw DownstreamUnavailableException propagating straight out of R4jPaymentService, no fallback, no state tracking — because there was no proxy in front of it at all. Adding org.aspectj:aspectjweaver fixed every failing test in the same run. spring-boot-starter-aop used to bundle this for you; its replacement doesn't, and nothing tells you that.

A naming trap in the dependency itself

resilience4j-spring-boot4 is a real, separate artifact from resilience4j-spring-boot3 (both currently at 2.4.0) — but its own POM depends on resilience4j-spring6, spring-core 7.0.2, spring-context 7.0.2 and spring-boot-autoconfigure 4.0.0. The "spring-boot4" in the artifact name is about which Boot generation it targets, not which internal Resilience4j module version it's built on — that module never got renamed to "spring7". If you're grepping your dependency tree for "spring7" expecting to find the pieces Boot 4.1 pulls in, you won't.

Next: 02-circuit-breaker.md — the Resilience4j side, verified against a real trip/recover run.