or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

admin-jmx.mdansi-support.mdaot-native-image.mdapplication-info.mdavailability.mdbootstrap.mdbootstrapping.mdbuilder.mdcloud-platform.mdconfiguration-annotations.mdconfiguration-data.mdconfiguration-properties.mdconversion.mddiagnostics.mdenvironment-property-sources.mdindex.mdjson-support.mdlifecycle-events.mdlogging.mdorigin-tracking.mdresource-loading.mdretry-support.mdssl-tls.mdstartup-metrics.mdsupport.mdsystem-utilities.mdtask-execution.mdthreading.mdutilities.mdvalidation.mdweb-support.md
tile.json

retry-support.mddocs/

Retry Support

Quick Reference

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.

Core Retry Components

ComponentPurposeThread SafetyKey Features
RetryPolicySettingsConfigure retry behaviorNOT thread-safe (builder)Fluent API, backoff, jitter, exception filtering
RetryPolicy (created)Execution retry policyThread-safeStateless, reusable across threads
RetryTemplateProgrammatic retry executionThread-safeCallback-based, listener support
@RetryableDeclarative retry annotationN/A (annotation)Method-level retry, backoff configuration
@EnableRetryEnable retry supportN/A (annotation)Class-level, enables AOP proxying

Backoff Strategies

StrategyConfigurationUse CaseExample
Fixed Delaydelay(Duration)Simple retry with constant waitdelay(Duration.ofSeconds(1))
Exponentialdelay() + multiplier()Progressive backoffdelay(1s).multiplier(2.0) → 1s, 2s, 4s, 8s
Exponential + Jitter+ randomFactor()Avoid thundering herdmultiplier(2.0).randomFactor(0.5)
Max Delay+ maxDelay()Cap exponential growthmaxDelay(Duration.ofSeconds(30))

Exception Filtering

MethodPurposeThread SafetyBehavior
retryableExceptions(List)Specify retryable exceptionsThread-safeOnly retry listed exceptions
nonRetryableExceptions(List)Specify non-retryable exceptionsThread-safeNever retry listed exceptions
exceptionFilter(Predicate)Custom exception filterThread-safeProgrammatic exception filtering

Configuration Properties

PropertyTypeDefaultDescription
maxRetrieslong3Maximum retry attempts (4 total including initial)
delayDuration1sInitial delay between retries
multiplierdouble1.0Backoff multiplier for exponential backoff
randomFactordouble0.0Jitter randomization factor (0.0-1.0)
maxDelayDurationnullMaximum 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.

Overview

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.

Core Components

RetryPolicySettings

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;
    }
}

Usage Examples

Basic Fixed Delay Retry

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";
    }
}

Exponential Backoff

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();
    }
}

Adding Jitter

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();
    }
}

Exception Filtering by Type

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();
    }
}

Custom Exception Predicate

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();
    }
}

Configuration Properties Integration

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.IllegalArgumentException
package 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;
    }
}

Using RetryTemplate

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";
            }
        );
    }
}

Custom Policy Factory

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();
    }
}

Production Best Practices

Circuit Breaker Integration

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";
    }
}

Monitoring and Metrics

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);
    }
}

Idempotency

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();
    }
}

Thread Safety

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.

Common Patterns

Database Operations

// Retry transient database errors
settings.setExceptionIncludes(List.of(
    SQLException.class,
    TransientDataAccessException.class
));
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofMillis(100));
settings.setMultiplier(2.0);

HTTP Calls

// 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));

Message Processing

// Retry with backoff, excluding poison messages
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(5));
settings.setExceptionExcludes(List.of(
    MessageParseException.class,
    InvalidMessageException.class
));

Best Practices

1. Start with Conservative Retry Counts

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.

2. Always Add Jitter for Distributed Systems

// GOOD - prevents thundering herd
settings.setMaxRetries(3L);
settings.setDelay(Duration.ofSeconds(1));
settings.setMultiplier(2.0);
settings.setJitter(Duration.ofMillis(500));  // ±500ms randomization

Rationale: When multiple instances retry simultaneously, they can overwhelm recovering services. Jitter spreads retry attempts over time.

3. Cap Exponential Backoff with maxDelay

// GOOD - exponential with cap
settings.setDelay(Duration.ofSeconds(1));
settings.setMultiplier(2.0);  // 1s, 2s, 4s, 8s...
settings.setMaxDelay(Duration.ofSeconds(30));  // Cap at 30s

Rationale: Uncapped exponential backoff can lead to extremely long delays (minutes), causing poor user experience and resource waste.

4. Be Specific with Exception Filters

// 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.

5. Combine with Circuit Breaker Pattern

@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.

6. Monitor Retry Metrics

@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.

7. Use Idempotent Operations

// 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).

8. Configure Per Operation Type

@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.

9. Add Timeout Alongside Retry

@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.

10. Document Retry Configuration

# 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.ResourceAccessException

Rationale: Clear documentation helps team members understand retry behavior and make informed adjustments.

Common Pitfalls

1. Retry Without Exponential Backoff - Overwhelming Failing Service

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 service

Solution: 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 recover

2. Retrying Non-Idempotent Operations - Data Duplication

Problem: 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;
    });
}

3. No Jitter in Multi-Instance Deployments - Thundering Herd

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.

4. Retrying Client Errors (4xx) - Wasted Effort

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
));

5. Infinite Exponential Backoff - Extreme Delays

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, 30s

6. No Retry Timeout - Infinite Wait

Problem: 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);

7. Retrying Inside Transaction - Holding DB Connection

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;
    });
}

8. Not Logging Retry Attempts - Hard to Debug

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());
        }
    }
});

9. Mixing Retry with Circuit Breaker Incorrectly

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 unnecessarily

Solution: 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 attempts

10. Forgetting Exception Predicate Evaluation Order

Problem: 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;
});

Related Documentation

  • Spring Retry Documentation
  • Circuit Breaker Pattern
  • Resilience4j Integration

Import Statements

// 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;