Files
spring-boot-demo/resilience/docs/06-what-is-left-for-resilience4j.md
asmhatreandClaude Opus 5 7e1676c763 Add resilience: Spring Framework 7 @Retryable and @ConcurrencyLimit
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
2026-09-11 17:12:25 +00:00

69 lines
3.5 KiB
Markdown

# 6. What is left for Resilience4j
[← 5. Retry and transactions](05-retry-and-transactions.md) · [Index](../README.md) · Next: [7. Migrating from spring-retry →](07-migrating-from-spring-retry.md)
| Need | Spring Framework 7.0.9 | Resilience4j 2.4.0 |
|---|---|---|
| Retry with back-off and jitter | `@Retryable`, `RetryTemplate` | `@Retry` |
| Limit concurrent calls | `@ConcurrencyLimit` (BLOCK/REJECT) | `@Bulkhead` (semaphore or thread pool, with a wait timeout) |
| Circuit breaker | - | `@CircuitBreaker` |
| Rate limiter (calls per period) | - | `@RateLimiter` |
| Time limit on a call | - (`timeout` is a retry budget) | `@TimeLimiter` (async return types) |
| Declarative fallback / recovery | - (`@Recover` request declined, spring-framework#35685) | `fallbackMethod` |
| Metrics | none built in | `resilience4j.*` meters, Actuator endpoints, health |
| Per-instance config in properties | placeholders in `*String` attributes | `resilience4j.<module>.instances.<name>.*` |
## The breaker, measured
`sliding-window-size: 10`, `minimum-number-of-calls: 5`, `failure-rate-threshold: 50`, 2 s open
([`r4j-breaker.txt`](output/r4j-breaker.txt)): five failures reach the downstream, the breaker
opens, calls 6-8 fail in microseconds with `CallNotPermittedException`, and after 2.1 s the first
call goes through (`HALF_OPEN`), the second closes it.
**Resilience4j's own defaults are `slidingWindowSize=100` and `minimumNumberOfCalls=100`** (read
from `CircuitBreakerConfig.ofDefaults()`), with 60 s open. A breaker left on defaults cannot open
until it has seen a hundred calls - in a low-traffic service, effectively never.
## `maxAttempts` vs `maxRetries`
[`r4j-retry.txt`](output/r4j-retry.txt): `max-attempts: 3`**3** invocations. Spring's
`maxRetries = 3`**4**. Same number, different meaning.
## Both libraries on one method
`chargeWithBoth()` carries `@CircuitBreaker(name="combo")` and Spring's `@Retryable(maxRetries=3, delay=10)`
([`r4j-combo.txt`](output/r4j-combo.txt), [`proxy-payments.txt`](output/proxy-payments.txt)):
```
"call 1": {
"caller got": "TransientException: payment gateway 503 (call 4)",
"method body ran": 4,
"elapsed (ms)": 35,
"breaker: buffered / failed / not permitted": "4 / 4 / 0",
"breaker state": "CLOSED"
},
"call 2": {
"caller got": "CallNotPermittedException: CircuitBreaker 'combo' is OPEN and does not permit further calls",
"method body ran": 1,
"elapsed (ms)": 33,
"breaker: buffered / failed / not permitted": "5 / 5 / 3",
"breaker state": "OPEN"
},
```
Spring's retry interceptor is the first advisor on the proxy - ahead of every Resilience4j aspect
(orders 2147483642-2147483646) - so it retries *around* the breaker. The breaker counts each attempt
as a separate call, and once it opens, the retry spends its remaining attempts on
`CallNotPermittedException`. With the default 1 s delay that is three seconds of waiting to be told
the circuit is open.
If you combine them, add `excludes = CallNotPermittedException.class` to the `@Retryable` - or use
Resilience4j's `@Retry`, whose aspect order puts it outside the breaker deliberately and which you
can configure to ignore `CallNotPermittedException`.
## TimeLimiter
[`r4j-timelimiter.txt`](output/r4j-timelimiter.txt): a 2 s `CompletableFuture` under a 500 ms
limit fails with `TimeoutException` after 512 ms. It only applies to asynchronous return types -
there is no equivalent for a blocking method in either library.