Resilience

Retries and concurrency limits moved into the core framework in Spring 7 — no extra project, no Resilience4j needed for the common cases.

Enable it

@Configuration
@EnableResilientMethods
class ResilienceConfig {}

@Retryable

import org.springframework.resilience.annotation.Retryable;

@Retryable
public void sendNotification() {  }             // 3 retries, 1 s apart
@Retryable(
    includes = RemoteServiceException.class,
    maxRetries = 4,
    delay = 100,          // ms
    jitter = 10,          // random spread, avoids thundering herds
    multiplier = 2,       // exponential backoff: 100, 200, 400, 800
    maxDelay = 1000)
public Book fetch(long id) {  }
AttributeDefaultMeaning
maxRetries3attempts after the first one
delay1000milliseconds before retrying
jitter0random addition to the delay
multiplier1backoff factor
maxDelaycap for the growing delay
includes / excludesall / nonewhich exceptions to retry

Reactive return types are retried too:

@Retryable(maxRetries = 4, delay = 100)
public Mono<Void> publish() {  }
Warning

Only retry idempotent operations. A retried POST /payments can charge twice — send an idempotency key, or do not retry.

Programmatic retries

When the annotation does not fit — a lambda, a loop body, a non-bean call:

var policy = RetryPolicy.builder()
        .includes(RemoteServiceException.class)
        .maxRetries(4)
        .delay(Duration.ofMillis(100))
        .multiplier(2)
        .maxDelay(Duration.ofSeconds(1))
        .build();

var template = new RetryTemplate(policy);
var book = template.invoke(() -> api.byId(id));

@ConcurrencyLimit

Caps how many threads may be inside a method at once — useful in front of a fragile dependency or a bounded pool.

import org.springframework.resilience.annotation.ConcurrencyLimit;

@ConcurrencyLimit(10)
public void callLegacySystem() {  }

@ConcurrencyLimit(1)                     // lock-like: one at a time
public void rebuildIndex() {  }

@ConcurrencyLimit(limitString = "${app.concurrency.limit:10}")
public void export() {  }

Callers beyond the limit wait — this is backpressure, not rejection.

Timeouts first

Retries multiply load. Before adding one, make sure the call can fail fast:

spring.http.clients.connect-timeout=1s
spring.http.clients.read-timeout=2s

A 30 s timeout with 4 retries means a client waiting two minutes for an error.

When you need more

Circuit breakers, bulkheads and rate limiters still live in Resilience4j (spring-cloud-starter-circuitbreaker-resilience4j). Reach for it when a failing dependency must be cut off entirely, not just retried.

★ Exercises

  1. Enable resilient methods and add @Retryable to a flaky call.
  2. Log every attempt and confirm the exponential backoff in the timestamps.
  3. Restrict retries to one exception type with includes.
  4. Put @ConcurrencyLimit(2) on a slow method and fire 10 parallel requests.
  5. Name two operations in your app that must never be retried.