docs
This documentation has been enhanced for AI coding agents with comprehensive examples, complete API signatures, thread safety notes, error handling patterns, and production-ready usage patterns.
| Component | Purpose | Thread Safety | Key Features |
|---|---|---|---|
RetryPolicySettings | Configure retry behavior | NOT thread-safe (builder) | Fluent API, backoff, jitter, exception filtering |
RetryPolicy (created) | Execution retry policy | Thread-safe | Stateless, reusable across threads |
RetryTemplate | Programmatic retry execution | Thread-safe | Callback-based, listener support |
@Retryable | Declarative retry annotation | N/A (annotation) | Method-level retry, backoff configuration |
@EnableRetry | Enable retry support | N/A (annotation) | Class-level, enables AOP proxying |
| Strategy | Configuration | Use Case | Example |
|---|---|---|---|
| Fixed Delay | delay(Duration) | Simple retry with constant wait | delay(Duration.ofSeconds(1)) |
| Exponential | delay() + multiplier() | Progressive backoff | delay(1s).multiplier(2.0) → 1s, 2s, 4s, 8s |
| Exponential + Jitter | + randomFactor() | Avoid thundering herd | multiplier(2.0).randomFactor(0.5) |
| Max Delay | + maxDelay() | Cap exponential growth | maxDelay(Duration.ofSeconds(30)) |
| Method | Purpose | Thread Safety | Behavior |
|---|---|---|---|
retryableExceptions(List) | Specify retryable exceptions | Thread-safe | Only retry listed exceptions |
nonRetryableExceptions(List) | Specify non-retryable exceptions | Thread-safe | Never retry listed exceptions |
exceptionFilter(Predicate) | Custom exception filter | Thread-safe | Programmatic exception filtering |
| Property | Type | Default | Description |
|---|---|---|---|
maxRetries | long | 3 | Maximum retry attempts (4 total including initial) |
delay | Duration | 1s | Initial delay between retries |
multiplier | double | 1.0 | Backoff multiplier for exponential backoff |
randomFactor | double | 0.0 | Jitter randomization factor (0.0-1.0) |
maxDelay | Duration | null | Maximum delay cap for exponential backoff |
Package: org.springframework.boot.retry
Module: org.springframework.boot:spring-boot
Since: 4.0.0
Configurable retry policy support for handling transient failures with exponential backoff, jitter, and exception filtering. This provides integration with Spring Retry for building resilient applications that can automatically recover from temporary errors.
Spring Boot's retry support simplifies the configuration of retry policies through a property-based settings class that integrates with Spring Retry's RetryPolicy. The system supports exponential backoff, randomized jitter, exception filtering, and custom policy factories.
Configuration class for creating retry policies with customizable backoff strategies, exception filtering, and jitter support.
package org.springframework.boot.retry;
import java.time.Duration;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import org.springframework.core.retry.RetryPolicy;
/**
* Settings for a RetryPolicy. Provides a fluent API for configuring
* retry behavior including delays, backoff multipliers, exception filters,
* and jitter.
*
* Thread Safety: This class is NOT thread-safe. Configure settings before
* creating the RetryPolicy. The created RetryPolicy is thread-safe.
*
* @since 4.0.0
*/
public final class RetryPolicySettings {
/**
* Default number of retry attempts (3).
* This means: 1 initial attempt + 3 retries = 4 total attempts.
*/
public static final long DEFAULT_MAX_RETRIES = 3L;
/**
* Default initial delay (1000ms = 1 second).
* Applied after the first failure, before the first retry.
*/
public static final Duration DEFAULT_DELAY = Duration.ofMillis(1000);
/**
* Default multiplier (1.0 - fixed delay).
* With multiplier = 1.0, all retry delays are the same.
* With multiplier > 1.0, delays increase exponentially.
*/
public static final double DEFAULT_MULTIPLIER = 1.0;
/**
* Default maximum delay (Long.MAX_VALUE milliseconds).
* Effectively unlimited by default.
*/
public static final Duration DEFAULT_MAX_DELAY = Duration.ofMillis(Long.MAX_VALUE);
/**
* Create a RetryPolicy based on the state of this instance.
* The policy includes all configured settings: max retries, delays,
* exception filters, jitter, and custom factory modifications.
*
* @return a configured RetryPolicy ready for use
* @throws IllegalStateException if configuration is invalid
*/
public RetryPolicy createRetryPolicy() {
// Implementation creates policy with all settings
}
/**
* Return the applicable exception types to attempt a retry for.
* If empty, all exceptions trigger retries (unless excluded).
* If non-empty, only these exception types (and subtypes) trigger retries.
*
* @return the list of exception types to retry (never null, may be empty)
*/
public List<Class<? extends Throwable>> getExceptionIncludes() {
return this.exceptionIncludes;
}
/**
* Set the applicable exception types to attempt a retry for.
* Only these exceptions (and subtypes) will trigger retries.
* Empty list means retry for any exception (default behavior).
*
* @param includes the exception types to retry (null treated as empty list)
*/
public void setExceptionIncludes(List<Class<? extends Throwable>> includes) {
this.exceptionIncludes = (includes != null) ? includes : List.of();
}
/**
* Return the non-applicable exception types to avoid a retry for.
* These exceptions will never trigger a retry, even if they match includes.
* Excludes take precedence over includes.
*
* @return the list of exception types to not retry (never null, may be empty)
*/
public List<Class<? extends Throwable>> getExceptionExcludes() {
return this.exceptionExcludes;
}
/**
* Set the non-applicable exception types to avoid a retry for.
* These exceptions will not trigger retries, regardless of includes.
*
* @param excludes the exception types to not retry (null treated as empty list)
*/
public void setExceptionExcludes(List<Class<? extends Throwable>> excludes) {
this.exceptionExcludes = (excludes != null) ? excludes : List.of();
}
/**
* Return the predicate to use to determine whether to retry based on Throwable.
* Provides fine-grained control beyond simple type matching.
* Returns null if no predicate is set.
*
* @return the predicate to use, or null if not configured
*/
public Predicate<Throwable> getExceptionPredicate() {
return this.exceptionPredicate;
}
/**
* Set the predicate to determine whether to retry based on Throwable.
* The predicate is evaluated for each exception. If it returns true,
* the exception is retryable (subject to includes/excludes).
*
* Example: retry only if message contains "transient"
*
* @param exceptionPredicate the predicate to use (null to clear)
*/
public void setExceptionPredicate(Predicate<Throwable> exceptionPredicate) {
this.exceptionPredicate = exceptionPredicate;
}
/**
* Return the maximum number of retry attempts.
* This is the number of retries AFTER the initial attempt.
* Total attempts = 1 (initial) + maxRetries.
*
* @return the maximum number of retry attempts (never null)
*/
public Long getMaxRetries() {
return this.maxRetries;
}
/**
* Set the maximum number of retry attempts.
* Must be >= 0. A value of 0 means no retries (only initial attempt).
*
* @param maxRetries the maximum number of retry attempts (must be >= 0)
* @throws IllegalArgumentException if maxRetries < 0
*/
public void setMaxRetries(Long maxRetries) {
if (maxRetries != null && maxRetries < 0) {
throw new IllegalArgumentException("maxRetries must be >= 0");
}
this.maxRetries = (maxRetries != null) ? maxRetries : DEFAULT_MAX_RETRIES;
}
/**
* Return the base delay after the initial invocation.
* This is the delay before the first retry.
*
* @return the base delay (never null)
*/
public Duration getDelay() {
return this.delay;
}
/**
* Set the base delay after the initial invocation.
* If a multiplier is specified, this serves as the initial delay,
* and subsequent delays are calculated as: delay * (multiplier ^ attemptNumber).
*
* @param delay the base delay (must be >= 0, null uses default)
* @throws IllegalArgumentException if delay is negative
*/
public void setDelay(Duration delay) {
if (delay != null && delay.isNegative()) {
throw new IllegalArgumentException("delay must not be negative");
}
this.delay = (delay != null) ? delay : DEFAULT_DELAY;
}
/**
* Return the jitter period for random retry attempts.
* Jitter adds randomness to prevent thundering herd problems.
* Returns null if no jitter is configured.
*
* Formula: actual_delay = delay ± random(0, jitter)
*
* @return the jitter duration, or null if not configured
*/
public Duration getJitter() {
return this.jitter;
}
/**
* Set a jitter period for the base retry attempt.
* Randomly subtracts or adds to the calculated delay, resulting in
* a value between (delay - jitter) and (delay + jitter).
*
* Use jitter to prevent multiple clients from retrying simultaneously.
*
* @param jitter the jitter duration (must be positive, or null to disable)
* @throws IllegalArgumentException if jitter is negative
*/
public void setJitter(Duration jitter) {
if (jitter != null && jitter.isNegative()) {
throw new IllegalArgumentException("jitter must be positive");
}
this.jitter = jitter;
}
/**
* Return the multiplier for the current interval for each attempt.
* Default is 1.0 (fixed delay).
*
* Examples:
* - 1.0 = fixed delay (1s, 1s, 1s, ...)
* - 2.0 = exponential backoff (1s, 2s, 4s, 8s, ...)
* - 1.5 = moderate backoff (1s, 1.5s, 2.25s, 3.375s, ...)
*
* @return the multiplier (never null, >= 1.0)
*/
public Double getMultiplier() {
return this.multiplier;
}
/**
* Set a multiplier for delay for the next retry attempt.
* Each retry delay is calculated as: previousDelay * multiplier.
*
* @param multiplier value to multiply the current interval (must be >= 1.0)
* @throws IllegalArgumentException if multiplier < 1.0
*/
public void setMultiplier(Double multiplier) {
if (multiplier != null && multiplier < 1.0) {
throw new IllegalArgumentException("multiplier must be >= 1.0");
}
this.multiplier = (multiplier != null) ? multiplier : DEFAULT_MULTIPLIER;
}
/**
* Return the maximum delay for any retry attempt.
* Limits how far jitter and multiplier can increase the delay.
*
* @return the maximum delay (never null)
*/
public Duration getMaxDelay() {
return this.maxDelay;
}
/**
* Set the maximum delay for any retry attempt.
* Prevents exponential backoff from growing indefinitely.
*
* @param maxDelay the maximum delay (must be positive, null uses default)
* @throws IllegalArgumentException if maxDelay is non-positive
*/
public void setMaxDelay(Duration maxDelay) {
if (maxDelay != null && !maxDelay.isPositive()) {
throw new IllegalArgumentException("maxDelay must be positive");
}
this.maxDelay = (maxDelay != null) ? maxDelay : DEFAULT_MAX_DELAY;
}
/**
* Set the factory to use to create the RetryPolicy.
* The function receives a RetryPolicy.Builder initialized with this instance's state.
* Use this for advanced customization beyond what settings provide.
*
* Example: builder -> builder.notOn(IllegalArgumentException.class).build()
*
* @param factory a factory to customize the retry policy (null to use default factory)
*/
public void setFactory(Function<RetryPolicy.Builder, RetryPolicy> factory) {
this.factory = factory;
}
}Simple retry with fixed 1-second delays:
package com.example.retry;
import org.springframework.boot.retry.RetryPolicySettings;
import org.springframework.core.retry.RetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import java.time.Duration;
/**
* Basic retry configuration with fixed delays.
*/
public class BasicRetryExample {
public RetryTemplate createRetryTemplate() {
// Configure retry settings
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(3L); // Try up to 3 times after initial attempt
settings.setDelay(Duration.ofSeconds(1)); // Wait 1 second between retries
// Create retry policy
RetryPolicy policy = settings.createRetryPolicy();
// Build retry template
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(policy);
return template;
}
public void useRetry(RetryTemplate retryTemplate) {
// Execute with retry
String result = retryTemplate.execute(context -> {
System.out.println("Attempt " + (context.getRetryCount() + 1));
// This will be retried on failure
return callExternalService();
});
System.out.println("Result: " + result);
}
private String callExternalService() {
// Simulated external call that may fail
if (Math.random() < 0.7) {
throw new RuntimeException("Service temporarily unavailable");
}
return "Success";
}
}Retry with exponentially increasing delays:
package com.example.retry;
import org.springframework.boot.retry.RetryPolicySettings;
import org.springframework.core.retry.RetryPolicy;
import java.time.Duration;
/**
* Exponential backoff retry configuration.
* Delays: 1s, 2s, 4s, 8s, 16s (capped at maxDelay)
*/
public class ExponentialBackoffExample {
public RetryPolicy createExponentialBackoffPolicy() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(5L); // Up to 5 retries
settings.setDelay(Duration.ofSeconds(1)); // Start with 1 second
settings.setMultiplier(2.0); // Double each time
settings.setMaxDelay(Duration.ofSeconds(30)); // Cap at 30 seconds
return settings.createRetryPolicy();
}
/**
* Moderate exponential backoff.
* Delays: 1s, 1.5s, 2.25s, 3.375s, 5.06s
*/
public RetryPolicy createModerateBackoffPolicy() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(5L);
settings.setDelay(Duration.ofSeconds(1));
settings.setMultiplier(1.5); // More gradual increase
settings.setMaxDelay(Duration.ofSeconds(10));
return settings.createRetryPolicy();
}
}Add randomness to prevent thundering herd:
package com.example.retry;
import org.springframework.boot.retry.RetryPolicySettings;
import org.springframework.core.retry.RetryPolicy;
import java.time.Duration;
/**
* Retry with jitter to randomize delays.
* Prevents multiple clients from retrying simultaneously.
*/
public class JitterExample {
public RetryPolicy createPolicyWithJitter() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(2));
settings.setJitter(Duration.ofMillis(500)); // ±500ms randomness
// Each retry will have a delay between 1.5s and 2.5s
return settings.createRetryPolicy();
}
/**
* Exponential backoff with jitter.
* Combines increasing delays with randomization.
*/
public RetryPolicy createExponentialWithJitter() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(5L);
settings.setDelay(Duration.ofSeconds(1));
settings.setMultiplier(2.0);
settings.setMaxDelay(Duration.ofSeconds(30));
settings.setJitter(Duration.ofMillis(200)); // Add ±200ms jitter
// Delays: ~1s, ~2s, ~4s, ~8s, ~16s (each with ±200ms variation)
return settings.createRetryPolicy();
}
}Retry only for specific exception types:
package com.example.retry;
import org.springframework.boot.retry.RetryPolicySettings;
import org.springframework.core.retry.RetryPolicy;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.ConnectException;
import java.time.Duration;
import java.util.List;
/**
* Retry configuration with exception type filtering.
*/
public class ExceptionFilteringExample {
/**
* Retry only for network-related exceptions.
*/
public RetryPolicy createNetworkRetryPolicy() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(1));
// Only retry for these exception types
settings.setExceptionIncludes(List.of(
IOException.class,
SocketTimeoutException.class,
ConnectException.class
));
return settings.createRetryPolicy();
}
/**
* Retry for all exceptions EXCEPT specific types.
*/
public RetryPolicy createWithExclusions() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(1));
// Don't retry for these exception types
settings.setExceptionExcludes(List.of(
IllegalArgumentException.class, // Bad input - don't retry
SecurityException.class, // Auth failure - don't retry
NullPointerException.class // Programming error - don't retry
));
return settings.createRetryPolicy();
}
/**
* Complex filtering: include some, exclude others.
*/
public RetryPolicy createComplexFiltering() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(1));
// Retry for IOExceptions
settings.setExceptionIncludes(List.of(IOException.class));
// But NOT for these specific IOExceptions
settings.setExceptionExcludes(List.of(
java.io.FileNotFoundException.class, // File missing - don't retry
java.nio.file.NoSuchFileException.class
));
// Excludes take precedence over includes
return settings.createRetryPolicy();
}
}Fine-grained exception filtering with predicates:
package com.example.retry;
import org.springframework.boot.retry.RetryPolicySettings;
import org.springframework.core.retry.RetryPolicy;
import java.time.Duration;
import java.sql.SQLException;
/**
* Retry configuration with custom exception predicate.
*/
public class PredicateFilteringExample {
/**
* Retry only if exception message contains specific keywords.
*/
public RetryPolicy createMessageBasedRetry() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(1));
// Retry only if message contains "transient" or "timeout"
settings.setExceptionPredicate(throwable -> {
String message = throwable.getMessage();
return message != null &&
(message.toLowerCase().contains("transient") ||
message.toLowerCase().contains("timeout") ||
message.toLowerCase().contains("temporary"));
});
return settings.createRetryPolicy();
}
/**
* Retry based on SQL error codes.
*/
public RetryPolicy createSqlRetryPolicy() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(2));
// Retry only for specific SQL states (transient errors)
settings.setExceptionPredicate(throwable -> {
if (throwable instanceof SQLException) {
SQLException sqlEx = (SQLException) throwable;
String sqlState = sqlEx.getSQLState();
// Retry for connection errors and deadlocks
return "08000".equals(sqlState) || // Connection exception
"08001".equals(sqlState) || // Unable to connect
"40001".equals(sqlState); // Serialization failure/deadlock
}
return false;
});
return settings.createRetryPolicy();
}
/**
* Complex predicate with multiple conditions.
*/
public RetryPolicy createComplexPredicate() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(5L);
settings.setDelay(Duration.ofMillis(500));
settings.setMultiplier(1.5);
settings.setExceptionPredicate(throwable -> {
// Check exception type
if (!(throwable instanceof RuntimeException)) {
return false;
}
// Check message content
String message = throwable.getMessage();
if (message == null) {
return false;
}
// Check if it's a transient error
boolean isTransient = message.contains("transient") ||
message.contains("temporary") ||
message.contains("unavailable");
// Check cause chain for specific exceptions
Throwable cause = throwable.getCause();
boolean hasRetryableCause = false;
while (cause != null) {
if (cause instanceof java.net.SocketException) {
hasRetryableCause = true;
break;
}
cause = cause.getCause();
}
return isTransient || hasRetryableCause;
});
return settings.createRetryPolicy();
}
}Bind retry settings to application properties:
# application.yml
app:
retry:
max-retries: 5
delay: 1s
multiplier: 1.5
max-delay: 30s
jitter: 200ms
exception-includes:
- java.io.IOException
- java.net.SocketTimeoutException
exception-excludes:
- java.lang.IllegalArgumentExceptionpackage com.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.retry.RetryPolicySettings;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.retry.RetryPolicy;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.support.RetryTemplate;
/**
* Configuration for retry support using application properties.
*/
@Configuration
@EnableRetry
public class RetryConfiguration {
/**
* Bind retry settings from application properties.
*
* @return retry policy settings
*/
@Bean
@ConfigurationProperties(prefix = "app.retry")
public RetryPolicySettings retryPolicySettings() {
return new RetryPolicySettings();
}
/**
* Create retry policy from settings.
*
* @param settings the retry policy settings
* @return configured retry policy
*/
@Bean
public RetryPolicy retryPolicy(RetryPolicySettings settings) {
return settings.createRetryPolicy();
}
/**
* Create retry template with configured policy.
*
* @param retryPolicy the retry policy
* @return retry template
*/
@Bean
public RetryTemplate retryTemplate(RetryPolicy retryPolicy) {
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(retryPolicy);
return template;
}
}Apply retry to operations:
package com.example.service;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* Service that uses retry template for resilient external calls.
*/
@Service
public class ExternalApiService {
private final RetryTemplate retryTemplate;
private final RestTemplate restTemplate;
public ExternalApiService(RetryTemplate retryTemplate, RestTemplate restTemplate) {
this.retryTemplate = retryTemplate;
this.restTemplate = restTemplate;
}
/**
* Call external API with automatic retry on failure.
*
* @param endpoint the API endpoint
* @return the response
*/
public String callApiWithRetry(String endpoint) {
return retryTemplate.execute(context -> {
System.out.printf("Attempt %d/%d%n",
context.getRetryCount() + 1,
context.getRetryPolicy().getMaxAttempts());
// This call will be retried on failure
return restTemplate.getForObject(endpoint, String.class);
});
}
/**
* Call with recovery callback on exhaustion.
*
* @param endpoint the API endpoint
* @return the response or fallback value
*/
public String callApiWithRecovery(String endpoint) {
return retryTemplate.execute(
// Main callback
context -> {
return restTemplate.getForObject(endpoint, String.class);
},
// Recovery callback (called after all retries exhausted)
context -> {
System.err.println("All retries exhausted, using fallback");
return "Fallback response";
}
);
}
}Advanced customization with custom factory:
package com.example.config;
import org.springframework.boot.retry.RetryPolicySettings;
import org.springframework.core.retry.RetryPolicy;
import java.time.Duration;
/**
* Advanced retry configuration with custom factory.
*/
public class CustomFactoryExample {
public RetryPolicy createCustomPolicy() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(1));
// Customize policy creation with factory
settings.setFactory(builder -> {
// The builder is pre-configured with settings
// Add additional customization here
return builder
.notOn(IllegalArgumentException.class) // Override: don't retry on IAE
.maxRetries(5L) // Override: use 5 retries instead
.build();
});
return settings.createRetryPolicy();
}
}Combine retry with circuit breaker for better resilience:
package com.example.resilience;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
/**
* Service combining retry and circuit breaker patterns.
*/
@Service
public class ResilientService {
private final RetryTemplate retryTemplate;
private final CircuitBreaker circuitBreaker;
public ResilientService(RetryTemplate retryTemplate) {
this.retryTemplate = retryTemplate;
// Configure circuit breaker
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // Open at 50% failure rate
.waitDurationInOpenState(Duration.ofSeconds(60)) // Wait 60s before half-open
.slidingWindowSize(10) // Track last 10 calls
.build();
this.circuitBreaker = CircuitBreaker.of("external-service", config);
}
/**
* Call with circuit breaker wrapping retry logic.
* Circuit breaker prevents retries when service is known to be down.
*
* @return the result
*/
public String resilientCall() {
return circuitBreaker.executeSupplier(() ->
retryTemplate.execute(context ->
performActualCall()
)
);
}
private String performActualCall() {
// Actual external service call
return "result";
}
}Track retry metrics for observability:
package com.example.monitoring;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component;
/**
* Retry listener that publishes metrics to Micrometer.
*/
@Component
public class RetryMetricsListener implements RetryListener {
private final Counter retryCounter;
private final Counter exhaustedCounter;
private final Timer retryTimer;
public RetryMetricsListener(MeterRegistry registry) {
this.retryCounter = Counter.builder("retry.attempts")
.description("Number of retry attempts")
.tag("type", "retry")
.register(registry);
this.exhaustedCounter = Counter.builder("retry.exhausted")
.description("Number of times retries were exhausted")
.tag("type", "exhausted")
.register(registry);
this.retryTimer = Timer.builder("retry.duration")
.description("Time spent in retry operations")
.register(registry);
}
@Override
public <T, E extends Throwable> void onError(
RetryContext context,
RetryCallback<T, E> callback,
Throwable throwable) {
retryCounter.increment();
}
@Override
public <T, E extends Throwable> void close(
RetryContext context,
RetryCallback<T, E> callback,
Throwable throwable) {
if (throwable != null && context.getRetryCount() > 0) {
exhaustedCounter.increment();
}
}
/**
* Register this listener with retry template.
*/
public void registerWith(RetryTemplate retryTemplate) {
retryTemplate.registerListener(this);
}
}Ensure operations are idempotent when retrying:
package com.example.service;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import java.util.UUID;
/**
* Service ensuring idempotent operations with retry.
*/
@Service
public class IdempotentPaymentService {
private final RetryTemplate retryTemplate;
private final PaymentGateway paymentGateway;
public IdempotentPaymentService(
RetryTemplate retryTemplate,
PaymentGateway paymentGateway) {
this.retryTemplate = retryTemplate;
this.paymentGateway = paymentGateway;
}
/**
* Process payment with idempotency key to prevent duplicate charges.
*
* @param request the payment request
* @return the payment result
*/
public PaymentResult processPayment(PaymentRequest request) {
// Generate idempotency key (or use one from request)
String idempotencyKey = generateIdempotencyKey(request);
return retryTemplate.execute(context -> {
// Include idempotency key in all attempts
// The gateway will deduplicate if we retry the same request
return paymentGateway.process(request, idempotencyKey);
});
}
private String generateIdempotencyKey(PaymentRequest request) {
// Generate based on request properties + timestamp
return UUID.nameUUIDFromBytes(
(request.getCustomerId() + "-" + request.getAmount()).getBytes()
).toString();
}
}RetryPolicySettings: NOT thread-safe during configuration. Configure all settings before calling createRetryPolicy(). The returned RetryPolicy IS thread-safe.
RetryPolicy: Thread-safe. Can be safely shared across multiple threads and used concurrently.
RetryTemplate: Thread-safe. A single instance can be shared and used by multiple threads simultaneously.
// Retry transient database errors
settings.setExceptionIncludes(List.of(
SQLException.class,
TransientDataAccessException.class
));
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofMillis(100));
settings.setMultiplier(2.0);// Retry network failures and 5xx errors
settings.setMaxRetries(5L);
settings.setDelay(Duration.ofSeconds(1));
settings.setMultiplier(1.5);
settings.setMaxDelay(Duration.ofSeconds(30));
settings.setJitter(Duration.ofMillis(200));// Retry with backoff, excluding poison messages
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(5));
settings.setExceptionExcludes(List.of(
MessageParseException.class,
InvalidMessageException.class
));Recommended: Begin with 3 retries (4 total attempts including initial).
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(3L); // 1 initial + 3 retries = 4 total attempts
settings.setDelay(Duration.ofSeconds(1));Rationale: More retries increase latency and can overwhelm failing systems. Start conservative and increase only if needed based on metrics.
// GOOD - prevents thundering herd
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(1));
settings.setMultiplier(2.0);
settings.setJitter(Duration.ofMillis(500)); // ±500ms randomizationRationale: When multiple instances retry simultaneously, they can overwhelm recovering services. Jitter spreads retry attempts over time.
// GOOD - exponential with cap
settings.setDelay(Duration.ofSeconds(1));
settings.setMultiplier(2.0); // 1s, 2s, 4s, 8s...
settings.setMaxDelay(Duration.ofSeconds(30)); // Cap at 30sRationale: Uncapped exponential backoff can lead to extremely long delays (minutes), causing poor user experience and resource waste.
// GOOD - retry only transient failures
settings.setExceptionIncludes(List.of(
SocketTimeoutException.class,
ConnectException.class,
HttpServerErrorException.class // 5xx errors
));
// Don't retry client errors
settings.setExceptionExcludes(List.of(
HttpClientErrorException.class, // 4xx errors
IllegalArgumentException.class,
ValidationException.class
));Rationale: Retrying non-transient errors (bad input, authentication failures) wastes resources and delays failure reporting.
@Configuration
public class ResilienceConfig {
@Bean
public RetryTemplate retryTemplate(RetryPolicySettings settings) {
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(settings.createRetryPolicy());
return template;
}
@Bean
public CircuitBreakerRetryPolicy circuitBreakerRetryPolicy() {
// Opens circuit after 5 consecutive failures
return new CircuitBreakerRetryPolicy(
retryPolicy,
5, // failure threshold
Duration.ofMinutes(1) // open duration
);
}
}Rationale: Circuit breakers prevent retry storms and give failing services time to recover.
@Component
public class RetryMetrics {
private final MeterRegistry registry;
private final Counter retryCounter;
private final Counter exhaustedCounter;
public RetryMetrics(MeterRegistry registry, RetryTemplate retryTemplate) {
this.registry = registry;
this.retryCounter = Counter.builder("retry.attempts")
.tag("operation", "external-api")
.register(registry);
this.exhaustedCounter = Counter.builder("retry.exhausted")
.register(registry);
retryTemplate.registerListener(new RetryListenerSupport() {
@Override
public <T, E extends Throwable> void onError(
RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
retryCounter.increment();
}
@Override
public <T, E extends Throwable> void onExhausted(
RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
exhaustedCounter.increment();
}
});
}
}Rationale: Metrics reveal retry patterns, help tune settings, and alert on systemic issues.
// GOOD - idempotent with request ID
@Service
public class PaymentService {
public void processPayment(PaymentRequest request) {
String idempotencyKey = request.getIdempotencyKey();
retryTemplate.execute(context -> {
// Include idempotency key in API call
return paymentGateway.charge(request, idempotencyKey);
});
}
}
// Generate idempotency key
public class PaymentRequest {
private String idempotencyKey = UUID.randomUUID().toString();
public String getIdempotencyKey() {
return idempotencyKey;
}
}Rationale: Idempotency ensures retries don't cause duplicate operations (double charges, duplicate records).
@Configuration
public class RetryConfig {
// Fast retries for quick operations
@Bean
@ConfigurationProperties("app.retry.database")
public RetryPolicySettings databaseRetrySettings() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(5L);
settings.setDelay(Duration.ofMillis(100));
return settings;
}
// Slower retries for external APIs
@Bean
@ConfigurationProperties("app.retry.external-api")
public RetryPolicySettings apiRetrySettings() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(2));
settings.setMultiplier(2.0);
return settings;
}
}Rationale: Different operations have different latency characteristics and failure modes requiring tailored retry strategies.
@Service
public class ResilientService {
private final RetryTemplate retryTemplate;
private final Duration timeout = Duration.ofSeconds(30);
public String callWithRetryAndTimeout() {
return retryTemplate.execute(context -> {
// Add timeout per attempt
CompletableFuture<String> future = CompletableFuture.supplyAsync(() ->
externalService.call()
);
try {
return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
throw new ServiceTimeoutException("Operation timed out", e);
}
});
}
}Rationale: Retries without timeouts can cause cascading delays. Each attempt should have a timeout.
# application.yml
app:
retry:
# Database connection retries
# Fast retries with no backoff - database should recover quickly
database:
max-retries: 5
delay: 100ms
multiplier: 1.0
# External API retries
# Exponential backoff with jitter - APIs may need time to recover
external-api:
max-retries: 3
delay: 1s
multiplier: 2.0
max-delay: 30s
jitter: 500ms
exception-includes:
- java.net.SocketTimeoutException
- org.springframework.web.client.ResourceAccessExceptionRationale: Clear documentation helps team members understand retry behavior and make informed adjustments.
Problem: Fixed delay retries hammer failing services, preventing recovery.
// WRONG - fixed 1s delay
settings.setMaxRetries(5L);
settings.setDelay(Duration.ofSeconds(1));
settings.setMultiplier(1.0); // No backoff!
// All retries hit in 5 seconds: t=0, t=1, t=2, t=3, t=4, t=5
// Overwhelms recovering serviceSolution: Use exponential backoff:
// CORRECT - exponential backoff
settings.setMaxRetries(5L);
settings.setDelay(Duration.ofSeconds(1));
settings.setMultiplier(2.0); // Double each time
settings.setMaxDelay(Duration.ofSeconds(30));
// Retries spread out: t=0, t=1, t=3, t=7, t=15, t=31
// Gives service time to recoverProblem: Retrying operations that aren't idempotent causes duplicates.
// WRONG - not idempotent
public void transferMoney(Account from, Account to, BigDecimal amount) {
retryTemplate.execute(context -> {
from.deduct(amount); // If retry after success, money deducted twice!
to.credit(amount);
return null;
});
}Solution: Make operations idempotent:
// CORRECT - idempotent with transaction ID
public void transferMoney(String transactionId, Account from, Account to, BigDecimal amount) {
retryTemplate.execute(context -> {
// Check if already processed
if (transactionRepository.exists(transactionId)) {
return transactionRepository.get(transactionId);
}
Transaction tx = new Transaction(transactionId);
from.deduct(amount);
to.credit(amount);
transactionRepository.save(tx);
return tx;
});
}Problem: All instances retry simultaneously, causing coordinated traffic spikes.
// WRONG - all instances retry at same time
settings.setDelay(Duration.ofSeconds(5));
settings.setMultiplier(2.0);
// No jitter!
// If 100 instances fail simultaneously:
// All retry at t=5s, t=15s, t=35s (coordinated spike!)Solution: Add jitter:
// CORRECT - randomize retry timing
settings.setDelay(Duration.ofSeconds(5));
settings.setMultiplier(2.0);
settings.setJitter(Duration.ofSeconds(2)); // ±2s randomization
// Retries spread across t=3s-7s, t=13s-17s, etc.Problem: Retrying non-transient errors that won't succeed.
// WRONG - retries everything including 400 Bad Request
settings.setMaxRetries(3L);
// No exception filtering
// Retries 400 Bad Request 3 times (will always fail!)Solution: Filter exceptions properly:
// CORRECT - only retry transient errors
settings.setExceptionIncludes(List.of(
SocketTimeoutException.class,
HttpServerErrorException.class // 5xx only
));
settings.setExceptionExcludes(List.of(
HttpClientErrorException.class // Don't retry 4xx
));Problem: No max delay cap causes extremely long waits.
// WRONG - unbounded exponential backoff
settings.setMaxRetries(10L);
settings.setDelay(Duration.ofSeconds(1));
settings.setMultiplier(2.0);
// No maxDelay!
// Delay progression: 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s
// Last retry waits 8.5 minutes!Solution: Cap maximum delay:
// CORRECT - capped backoff
settings.setMaxRetries(10L);
settings.setDelay(Duration.ofSeconds(1));
settings.setMultiplier(2.0);
settings.setMaxDelay(Duration.ofSeconds(30)); // Cap at 30s
// Delay progression: 1s, 2s, 4s, 8s, 16s, 30s, 30s, 30s, 30s, 30sProblem: Retry loop runs indefinitely if service never recovers.
// WRONG - no overall timeout
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(settings.createRetryPolicy());
// Could wait minutes or hours if service stays down!Solution: Add timeout policy:
// CORRECT - timeout after 2 minutes total
RetryTemplate template = new RetryTemplate();
CompositeRetryPolicy composite = new CompositeRetryPolicy();
composite.setPolicies(new RetryPolicy[]{
settings.createRetryPolicy(),
new TimeoutRetryPolicy(Duration.ofMinutes(2).toMillis())
});
template.setRetryPolicy(composite);Problem: Retry logic inside transaction holds connections during backoff.
// WRONG - transaction spans all retries
@Transactional
public void processOrder(Order order) {
retryTemplate.execute(context -> {
// DB connection held during all retries and delays!
externalService.charge(order);
orderRepository.save(order);
return null;
});
}Solution: Retry outside transaction:
// CORRECT - retry outside, transaction inside
public void processOrder(Order order) {
retryTemplate.execute(context -> {
// Retry external call
PaymentResult result = externalService.charge(order);
// Open transaction only for database work
executeInTransaction(() -> {
order.setPaymentResult(result);
orderRepository.save(order);
});
return result;
});
}Problem: Silent retries make issues hard to diagnose.
// WRONG - no visibility into retries
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(settings.createRetryPolicy());
// No listeners!Solution: Add logging listener:
// CORRECT - log all retry attempts
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(settings.createRetryPolicy());
template.registerListener(new RetryListenerSupport() {
private static final Logger log = LoggerFactory.getLogger(RetryListener.class);
@Override
public <T, E extends Throwable> void onError(
RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
log.warn("Retry attempt {} failed: {}",
context.getRetryCount(),
throwable.getMessage());
}
@Override
public <T, E extends Throwable> void close(
RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
if (context.getRetryCount() > 0) {
log.info("Operation succeeded after {} retries", context.getRetryCount());
}
}
});Problem: Retrying when circuit is open wastes time and resources.
// WRONG - retry even when circuit is open
@Retryable(maxAttempts = 3)
@CircuitBreaker(failureThreshold = 5)
public String callService() {
return externalService.call();
}
// When circuit opens, still retries 3 times unnecessarilySolution: Circuit breaker outside, retry inside:
// CORRECT - circuit breaker wraps retry
@CircuitBreaker(failureThreshold = 5, waitDurationInOpenState = 60000)
public String callService() {
return retryTemplate.execute(context -> {
return externalService.call();
});
}
// Circuit opens after 5 failures, skipping retry attemptsProblem: Exception filters conflict or override each other unexpectedly.
// WRONG - conflicting filters
settings.setExceptionIncludes(List.of(
RuntimeException.class // Includes ALL runtime exceptions
));
settings.setExceptionExcludes(List.of(
IllegalArgumentException.class // But exclude this subclass?
));
// IllegalArgumentException is a RuntimeException
// Which filter wins?Solution: Use either includes OR excludes, or use predicate:
// Option 1: Use only includes with specific exceptions
settings.setExceptionIncludes(List.of(
SocketTimeoutException.class,
IOException.class
));
// Option 2: Use only excludes
settings.setExceptionExcludes(List.of(
IllegalArgumentException.class,
IllegalStateException.class
));
// Option 3: Use predicate for complex logic
settings.setExceptionPredicate(throwable -> {
// Retry if:
// - Any IOException except FileNotFoundException
// - Any SocketException
if (throwable instanceof FileNotFoundException) {
return false;
}
return throwable instanceof IOException ||
throwable instanceof SocketException;
});// Core retry classes
import org.springframework.boot.retry.RetryPolicySettings;
import org.springframework.core.retry.RetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.annotation.Backoff;
// Java time
import java.time.Duration;
// Collections
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
// Configuration
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;