Files
spring-boot-demo/resilience
asmhatre 604291067e resilience: prove spring-boot-starter-aop was renamed to spring-boot-starter-aspectj, not removed
Maven Central's maven-metadata.xml for both artifact IDs, read together, shows aop's last
release is 4.0.0-M2 and aspectj's first release is 4.0.0-M3 -- a rename at a milestone
boundary, not a drop. The renamed starter's own published POM depends on exactly
spring-aop + aspectjweaver, the two jars this pom.xml's dependency comment already claimed
were needed; this commit adds the reproducible build-break proof, the metadata/POM evidence,
and a new doc chapter, and backs the corrected companion-repo links from the resilience4j-
circuit-breaker-spring-boot post.
2026-09-18 08:34:06 +00:00
..

Spring Framework 7's built-in resilience - and what is left for Resilience4j

Companion project for Spring Framework 7's Built-in Resilience: @Retryable, @ConcurrencyLimit, and What's Left for Resilience4j on ankurm.com.

Every behaviour the article states was counted here - by recording each real invocation of the guarded method, not by trusting the retry machinery's own view. ./scripts/run-all.sh regenerates every transcript in docs/output/.

Versions

Spring Boot 4.1.1
Spring Framework 7.0.9 (org.springframework.resilience, org.springframework.core.retry)
Resilience4j 2.4.0 (resilience4j-spring-boot4, not managed by Boot - pin it)
JDK Eclipse Temurin 25.0.4.1 (LTS)

Quickstart

export JAVA_HOME=/path/to/jdk-25
mvn -DskipTests package
./scripts/run.sh                                  # port 8081
curl -s localhost:8081/demo/retry/exhausted | python3 -m json.tool
./scripts/run-all.sh                              # about 90 s: some scenarios sleep through 1 s delays
mvn test                                          # 16 contract tests

Scenarios

Endpoint Shows
/demo/retry/{defaults,exhausted,exponential,jitter,includes,cause,future,mono,timeout,hang,self-invocation} what @Retryable does, counted
/demo/tx/{standalone,joined} @Retryable + @Transactional, alone and inside a caller's transaction
/demo/limit/{none,block,reject,limit-and-retry,r4j-bulkhead} @ConcurrencyLimit policies, and Resilience4j's bulkhead for comparison
/demo/r4j/{retry,breaker,combo,timelimiter} what only Resilience4j does, and both libraries on one method
/demo/proxy/{payments,stock-writer,reports} the advisor chain on each proxy, outermost first

All of these are diagnostics for the article. Delete DemoController before shipping anything.

Documentation

  1. What Spring Framework 7 ships, and in which 7.0.x release
  2. Switching it on - and the three ways it stays off
  3. @Retryable, measured
  4. @ConcurrencyLimit: BLOCK, REJECT, and nesting with retry
  5. Retry and transactions
  6. What is left for Resilience4j
  7. Migrating from spring-retry
  8. spring-boot-starter-aop was renamed, not removed - also backs the sibling post Resilience4j Circuit Breaker in Spring Boot 4.1

Findings worth the trip

  • maxRetries = 3 is four invocations. Resilience4j's maxAttempts: 3 is three, and so was spring-retry's maxAttempts. A mechanical migration adds one call.
  • timeout is a budget checked between attempts, not a call timeout. A 1.5 s attempt under a 500 ms timeout runs its full 1.5 s.
  • Jitter only ever adds delay (for the first delay, and for every delay when multiplier is 1): delay=200, jitter=100 produced gaps of 211-296 ms, never below 200.
  • A CompletableFuture that completes exceptionally is not retried; a Mono is.
  • @Retryable is placed outside @Transactional and outside Resilience4j regardless of any order - both post-processors call setBeforeExistingAdvisors(true). Good for transactions, bad for a circuit breaker: an open circuit is retried three times.
  • timeout and ConcurrencyLimit.ThrottlePolicy.REJECT did not exist in 7.0.0. timeout arrived in 7.0.2, policy in 7.0.3.