Introduction

Spring Retry provides declarative retry logic with @Retryable, but it silently does not retry when the thrown exception is not in the include list, when retry state is not properly managed, or when the backoff policy is misconfigured. The most common issue is that @Retryable only retries specific exception types -- if the wrong exception is thrown (a wrapper exception, or a subclass not in the retry list), Spring executes the method once and returns the error without any retry attempts.

Symptoms

bash
# Method called once, no retries observed
com.example.ServiceException: Connection refused
# Expected: 3 retry attempts with backoff

Or:

bash
org.springframework.retry.ExhaustedRetryException: Retry exhausted after 3 attempts
# But no backoff delay observed between retries

Common Causes

  • Exception not in include list: @Retryable does not include the thrown exception type
  • Exception in exclude list: Exception explicitly excluded from retry
  • noBackoff means instant retries: No delay between retries
  • Stateless retry loses context: Retry state not preserved across method calls
  • Max attempts set to 1: Only one execution, no retries
  • Retry not enabled: @EnableRetry missing from configuration

Step-by-Step Fix

Step 1: Configure @Retryable with proper exceptions and backoff

```java import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.retry.annotation.Recover;

@Service public class ExternalApiService {

@Retryable( value = {RestClientException.class, ConnectException.class}, // Exceptions to retry maxAttempts = 5, backoff = @Backoff( delay = 1000, // Initial delay: 1 second maxDelay = 30000, // Maximum delay: 30 seconds multiplier = 2.0, // Exponential: 1s, 2s, 4s, 8s, 16s random = true // Add jitter to prevent thundering herd ) ) public ApiResponse callExternalApi(String endpoint) { return restTemplate.getForObject(endpoint, ApiResponse.class); }

// Fallback method when all retries exhausted @Recover public ApiResponse recoverApiCall(RestClientException e, String endpoint) { log.error("API call failed after retries: {}", endpoint, e); return ApiResponse.fallback(); } } ```

Step 2: Enable retry and configure stateful retry

```java @Configuration @EnableRetry // REQUIRED for @Retryable to work public class RetryConfig {

// Global retry configuration @Bean public RetryTemplate retryTemplate() { RetryTemplate template = new RetryTemplate();

// Exponential backoff ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy(); backOff.setInitialInterval(1000); backOff.setMaxInterval(30000); backOff.setMultiplier(2.0); template.setBackOffPolicy(backOff);

// Retry policy SimpleRetryPolicy policy = new SimpleRetryPolicy(); policy.setMaxAttempts(5); Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>(); retryableExceptions.put(RestClientException.class, true); retryableExceptions.put(ConnectException.class, true); policy.setRetryableExceptions(retryableExceptions); template.setRetryPolicy(policy);

return template; } } ```

Step 3: Debug retry behavior

```java import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; import org.springframework.retry.listener.RetryListenerSupport;

public class LoggingRetryListener extends RetryListenerSupport {

@Override public <T, E extends Throwable> void close( RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { log.info("Retry complete: {} attempts, exception: {}", context.getRetryCount(), throwable != null ? throwable.getMessage() : "none"); }

@Override public <T, E extends Throwable> void onError( RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { log.warn("Retry attempt {} for {}: {}", context.getRetryCount(), callback.toString(), throwable.getMessage()); } } ```

Prevention

  • Always add @EnableRetry to configuration class
  • Specify exception types explicitly with value/include attributes
  • Use multiplier > 1.0 for exponential backoff to avoid thundering herd
  • Add random=true to backoff for jitter in distributed systems
  • Implement @Recover fallback methods for graceful degradation
  • Add retry listeners to log and monitor retry behavior
  • Test retry behavior by throwing exceptions in unit tests