Spring Boot
Retryable
Error Handling
Java
Annotations

Springboot retryable not retrying

Data Structures & Algorithms practice on Codemia

Step through 300+ coding interview problems with animated visualisers that show the data structure changing as the code runs. The Blind 75 are free.

Practice algorithms

Introduction

When @Retryable does not retry in Spring Boot, the most common causes are: missing @EnableRetry annotation, the spring-retry dependency not on the classpath, calling the retryable method from within the same class (bypassing the proxy), or the wrong exception type being thrown. The @Retryable annotation works through Spring AOP proxying — the method must be called on a Spring-managed bean from another bean for the retry interceptor to trigger.

Basic Working Setup

java
1// 1. Add dependency (build.gradle)
2// implementation 'org.springframework.retry:spring-retry'
3// implementation 'org.springframework.boot:spring-boot-starter-aop'
4
5// 2. Enable retry on a configuration class
6@Configuration
7@EnableRetry
8public class RetryConfig {
9}
10
11// 3. Annotate the method
12@Service
13public class ExternalApiService {
14
15    @Retryable(
16        retryFor = {RestClientException.class},
17        maxAttempts = 3,
18        backoff = @Backoff(delay = 1000, multiplier = 2)
19    )
20    public String callExternalApi() {
21        // Throws RestClientException on failure
22        return restTemplate.getForObject("https://api.example.com/data", String.class);
23    }
24
25    @Recover
26    public String recover(RestClientException e) {
27        return "Fallback response";
28    }
29}

Cause 1: Missing @EnableRetry

The most common cause. Without @EnableRetry, the @Retryable annotation is ignored:

java
1// WRONG — no @EnableRetry anywhere
2@SpringBootApplication
3public class MyApplication {
4    public static void main(String[] args) {
5        SpringApplication.run(MyApplication.class, args);
6    }
7}
8
9// FIX — add @EnableRetry
10@SpringBootApplication
11@EnableRetry
12public class MyApplication {
13    public static void main(String[] args) {
14        SpringApplication.run(MyApplication.class, args);
15    }
16}

Cause 2: Missing Dependencies

Both spring-retry and spring-boot-starter-aop are required:

groovy
1// build.gradle
2dependencies {
3    implementation 'org.springframework.retry:spring-retry'
4    implementation 'org.springframework.boot:spring-boot-starter-aop'
5}
xml
1<!-- pom.xml -->
2<dependency>
3    <groupId>org.springframework.retry</groupId>
4    <artifactId>spring-retry</artifactId>
5</dependency>
6<dependency>
7    <groupId>org.springframework.boot</groupId>
8    <artifactId>spring-boot-starter-aop</artifactId>
9</dependency>

Without spring-boot-starter-aop, the AOP proxy that intercepts the method call is not created.

Cause 3: Self-Invocation (Same Class Call)

Calling a @Retryable method from within the same class bypasses the Spring proxy:

java
1@Service
2public class MyService {
3
4    @Retryable(retryFor = RuntimeException.class, maxAttempts = 3)
5    public String fetchData() {
6        throw new RuntimeException("Temporary failure");
7    }
8
9    // WRONG — calling fetchData() from the same class bypasses the proxy
10    public String processData() {
11        return fetchData();  // No retry happens!
12    }
13}

Fix: inject the bean into itself or move the retryable method to a separate service:

java
1// Fix 1: Separate service
2@Service
3public class DataFetcher {
4
5    @Retryable(retryFor = RuntimeException.class, maxAttempts = 3)
6    public String fetchData() {
7        throw new RuntimeException("Temporary failure");
8    }
9}
10
11@Service
12public class MyService {
13
14    @Autowired
15    private DataFetcher dataFetcher;
16
17    public String processData() {
18        return dataFetcher.fetchData();  // Retry works — goes through proxy
19    }
20}
21
22// Fix 2: Self-injection (less clean but works)
23@Service
24public class MyService {
25
26    @Autowired
27    private MyService self;  // Inject proxy of self
28
29    @Retryable(retryFor = RuntimeException.class, maxAttempts = 3)
30    public String fetchData() {
31        throw new RuntimeException("Temporary failure");
32    }
33
34    public String processData() {
35        return self.fetchData();  // Goes through proxy — retry works
36    }
37}

Cause 4: Wrong Exception Type

@Retryable only retries for the specified exception types:

java
1// WRONG — retryFor specifies IOException, but method throws RuntimeException
2@Retryable(retryFor = IOException.class, maxAttempts = 3)
3public String fetchData() {
4    throw new RuntimeException("This won't be retried!");
5}
6
7// FIX — match the exception type
8@Retryable(retryFor = {RuntimeException.class, IOException.class}, maxAttempts = 3)
9public String fetchData() {
10    throw new RuntimeException("This will be retried");
11}
12
13// Or retry all exceptions
14@Retryable(maxAttempts = 3)  // Retries any Exception by default
15public String fetchData() {
16    throw new RuntimeException("This will be retried");
17}

Cause 5: Method Visibility

@Retryable only works on public methods (Spring AOP limitation with default proxies):

java
1// WRONG — private method, proxy cannot intercept
2@Retryable(maxAttempts = 3)
3private String fetchData() {  // No retry — not proxied
4    throw new RuntimeException("Failure");
5}
6
7// FIX — make it public
8@Retryable(maxAttempts = 3)
9public String fetchData() {  // Retry works
10    throw new RuntimeException("Failure");
11}

Cause 6: @Recover Method Mismatch

The @Recover method must match the return type and exception type:

java
1@Service
2public class MyService {
3
4    @Retryable(retryFor = RestClientException.class, maxAttempts = 3)
5    public String callApi(String url) {
6        return restTemplate.getForObject(url, String.class);
7    }
8
9    // WRONG — parameter type doesn't match
10    @Recover
11    public String recover(IOException e, String url) {  // Wrong exception type
12        return "fallback";
13    }
14
15    // CORRECT — must match: return type, exception type, then original params
16    @Recover
17    public String recover(RestClientException e, String url) {
18        return "fallback for " + url;
19    }
20}

Verifying Retry Works

java
1@Service
2public class TestableService {
3
4    private int attempts = 0;
5
6    @Retryable(retryFor = RuntimeException.class, maxAttempts = 3,
7               backoff = @Backoff(delay = 100))
8    public String unstableMethod() {
9        attempts++;
10        System.out.println("Attempt #" + attempts);
11        if (attempts < 3) {
12            throw new RuntimeException("Attempt " + attempts + " failed");
13        }
14        return "Success on attempt " + attempts;
15    }
16}
17
18// Test
19@SpringBootTest
20@EnableRetry
21class RetryTest {
22
23    @Autowired
24    private TestableService service;
25
26    @Test
27    void shouldRetryThreeTimes() {
28        String result = service.unstableMethod();
29        assertEquals("Success on attempt 3", result);
30    }
31}

Common Pitfalls

  • Calling @Retryable method from the same class: Spring AOP uses proxies that only intercept external method calls. When method A calls method B in the same class, B's @Retryable is bypassed. Move the retryable method to a separate @Service bean.
  • Missing spring-boot-starter-aop dependency: spring-retry alone is not enough. Without the AOP starter, Spring cannot create the proxy that intercepts method calls. Both spring-retry and spring-boot-starter-aop must be on the classpath.
  • @Recover method signature mismatch: The recovery method must have the same return type as the retryable method, take the exception as the first parameter, and take the same additional parameters in the same order. A mismatch causes Spring to silently skip the recovery method.
  • Using @Retryable on final or static methods: Spring's default proxy-based AOP cannot intercept final or static methods. If you need retry on a final method, switch to AspectJ weaving by setting @EnableRetry(proxyTargetClass = true) or use compile-time weaving.
  • Not specifying retryFor (relying on defaults): Without retryFor, @Retryable retries on any Exception but not Error. If your method throws a checked exception that you expect to be retried, explicitly listing it in retryFor makes the intent clear and avoids accidentally retrying non-transient exceptions.

Summary

  • Always add both @EnableRetry and the spring-retry + spring-boot-starter-aop dependencies
  • Never call a @Retryable method from within the same class — Spring's proxy cannot intercept self-invocations
  • Ensure the exception thrown matches the retryFor exception type
  • Make retryable methods public and non-final for proxy-based AOP to work
  • Match @Recover method signatures exactly: same return type, exception first, then original parameters

Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Data Structures & Algorithms practice on Codemia

Step through 300+ coding interview problems with animated visualisers that show the data structure changing as the code runs. The Blind 75 are free.

Practice algorithms