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).
Back to blog

Leave a comment

Please note, comments need to be approved before they are published.