Companion code for "Spring Framework 7's Built-in Resilience: @Retryable, @ConcurrencyLimit, and What's Left for Resilience4j". Every retry counted by recording real invocations: defaults, backoff and jitter, timeout, reactive and CompletableFuture returns, the concurrency limit's BLOCK and REJECT policies, retries around transactions, composition with Resilience4j 2.4.0, and the annotation API across 7.0.0-7.0.9. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
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
- What Spring Framework 7 ships, and in which 7.0.x release
- Switching it on - and the three ways it stays off
@Retryable, measured@ConcurrencyLimit: BLOCK, REJECT, and nesting with retry- Retry and transactions
- What is left for Resilience4j
- Migrating from spring-retry
Findings worth the trip
maxRetries = 3is four invocations. Resilience4j'smaxAttempts: 3is three, and so was spring-retry'smaxAttempts. A mechanical migration adds one call.timeoutis 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
multiplieris 1):delay=200, jitter=100produced gaps of 211-296 ms, never below 200. - A
CompletableFuturethat completes exceptionally is not retried; aMonois. @Retryableis placed outside@Transactionaland outside Resilience4j regardless of anyorder- both post-processors callsetBeforeExistingAdvisors(true). Good for transactions, bad for a circuit breaker: an open circuit is retried three times.timeoutandConcurrencyLimit.ThrottlePolicy.REJECTdid not exist in 7.0.0.timeoutarrived in 7.0.2,policyin 7.0.3.