Spring @Retryable
@Retryable
is from Spring Retry. It lets you automatically re-invoke a method when it throws specific exceptions—handy for flaky I/O (HTTP, DB, MQ), temporary locks, etc.
Quick setup
Dependencies
-
You need both AOP and Spring Retry.
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
Enable once
import org.springframework.retry.annotation.EnableRetry;
@SpringBootApplication
@EnableRetry
public class App { ... }
Basic usage
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class PaymentClient {
@Retryable(
include = {java.io.IOException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 500, multiplier = 2.0) // 500ms, 1s, 2s
)
public String callGateway(String payload) throws java.io.IOException {
// do HTTP call; throw IOException on transient failure
...
}
}
Fallback when retries are exhausted (@Recover
)
import org.springframework.retry.annotation.Recover;
@Service
public class PaymentClient {
// ... @Retryable method above ...
@Recover
public String recover(java.io.IOException ex, String payload) {
// final fallback (circuit to alt path, return cached response, etc.)
return "fallback";
}
}
-
@Recover
must be in the same bean, return the same type, and have the exception as the first parameter; remaining params match the retried method.
Useful options (most common)
-
maxAttempts
(default 3) backoff = @Backoff(delay=..., multiplier=..., maxDelay=..., random=true)
-
include
/exclude
→ which exceptions to retry - Class-level
@Retryable
to set defaults for all methods in the class.
With @Transactional (important)
- Place
@Transactional
on the same method so each attempt runs in a fresh transaction (rollback happens, then a new attempt starts). - Ensure the exception you want to retry on is marked for rollback (e.g.,
@Transactional(rollbackFor=YourException.class)
if it’s checked). - Don’t swallow exceptions inside the method; let them propagate to trigger the retry.
Common pitfalls
- Self-invocation: Calls from one method to another inside the same class bypass the proxy → retry won’t trigger. Call from another bean or refactor.
- Stateful side effects: Make your code idempotent (safe to run again) or compensate between attempts.
- Too aggressive retries: Always use backoff (prefer exponential + jitter) to avoid thundering herds.
Tiny real-world example (HTTP)
@Service
public class UserApi {
private final RestTemplate rest = new RestTemplate();
@Retryable(
include = {ResourceAccessException.class, HttpServerErrorException.class},
maxAttempts = 4,
backoff = @Backoff(delay = 300, multiplier = 2.0, maxDelay = 5000, random = true)
)
public UserDTO getUser(String id) {
return rest.getForObject("https://api.example.com/users/{id}", UserDTO.class, id);
}
@Recover
public UserDTO recover(Exception ex, String id) {
// e.g., serve from cache or return a stub
return null;
}
}
When not to use @Retryable
-
Reactive WebFlux: prefer Reactor’s
retryWhen
. -
Complex policies/circuit breaking: consider Resilience4j (
@Retry
,@CircuitBreaker
).