# 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..instances..*` | ## 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.