Spring Boot
Retryable
Error Handling
Java
Annotations

Springboot retryable not retrying

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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

Related reading
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
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.