Backend Development 9 min read

Master Spring Retry: Annotation & Programmatic Retries in SpringBoot 2.7

This tutorial explains how to use Spring Retry in a SpringBoot 2.7 application, covering both annotation‑based @Retryable usage and programmatic RetryTemplate configuration, with complete code examples, listener implementation, and execution results.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Spring Retry: Annotation & Programmatic Retries in SpringBoot 2.7

1. Introduction

Spring Retry is a component provided by the Spring framework for method retrying. It is built on Spring AOP and can handle method call failures caused by network issues or other transient problems. The core idea is to retry when a method fails according to a configured RetryPolicy , which defines whether a retry can be triggered, which exceptions are retryable, and how long to wait before retrying. Spring Retry offers various strategies such as BackOffPolicy . The @Retryable annotation can be placed directly on methods, and RetryTemplate provides more flexible control, while callbacks can be implemented via the RetryCallback interface.

2. Practical Example

2.1 Annotation‑based

Dependency

<code>&lt;dependencies&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.retry&lt;/groupId&gt;
    &lt;artifactId&gt;spring-retry&lt;/artifactId&gt;
  &lt;/dependency&gt;
&lt;/dependencies&gt;</code>

Entity and DAO

<code>@Entity
@Table(name = "t_account")
public class Account {
  @Id
  private Long id;
  private String userId;
  private BigDecimal money;
}</code>

Repository interface

<code>public interface AccountRepository extends JpaRepository&lt;Account, Long&gt; {
  Account findByUserId(String userId);
}</code>

Service class

<code>@Service
public class AccountService {
  @Resource
  private AccountRepository ar;

  @Transactional
  @Retryable(value = {InvalidParameterException.class})
  public Account save(Account account) {
    System.out.println(Thread.currentThread().getName());
    if (account.getId() == 0) {
      System.out.println("save retry...");
      throw new InvalidParameterException("Invalid parameter");
    }
    return ar.saveAndFlush(account);
  }

  // Callback after retries are exhausted
  @Recover
  public Account recoverSave(Exception e) {
    System.out.println("............." + e.getMessage());
    return null;
  }
}</code>

Annotation attributes description

@Retryable attributes include:

recover : name of the fallback method; if omitted, Spring matches a method annotated with @Recover whose first parameter is the retry exception.

interceptor : specify a method‑level bean implementing MethodInterceptor .

value / include : exception types to retry; if both are empty, all exceptions are retried.

exclude : exception types that should not be retried.

label : a unique label for statistics.

stateful : default false ; indicates whether retries are stateful.

maxAttempts : maximum retry attempts, default is 3.

backoff : specify @Backoff for delay strategy.

Backoff configuration illustration

Backoff strategy illustration
Backoff strategy illustration

listeners : specify beans implementing org.springframework.retry.RetryListener . Example listener implementation:

<code>public class AccountRetryListener implements RetryListener {
  @Override
  public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
    System.out.println("open invoke...");
    return false; // returning false prevents retry
  }

  @Override
  public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
    System.out.println("close invoke...");
  }

  @Override
  public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
    System.out.println("onError invoke...");
    throwable.getStackTrace();
  }
}</code>

Enabling retry functionality

<code>@Configuration
@EnableRetry
public class RetryConfig {
}</code>

Test case for annotation‑based retry

<code>@Resource
private AccountService accountService;

@Test
public void testSave() {
  Account account = new Account();
  account.setId(0L);
  account.setMoney(BigDecimal.valueOf(1000));
  account.setUserId("1");
  accountService.save(account);
}</code>

Console output shows three retry attempts (default maxAttempts = 3).

Console output showing retries
Console output showing retries

2.2 Programmatic (imperative) retry

Custom RetryTemplate configuration

<code>@Configuration
@EnableRetry
public class RetryConfig {
  @Bean
  public RetryTemplate retryTemplate() {
    return RetryTemplate.builder()
        .maxAttempts(3) // retry count
        .fixedBackoff(1000) // fixed interval 1000ms
        .retryOn(InvalidParameterException.class) // retry on specific exception
        .build();
  }
}</code>

Modified service using RetryTemplate

<code>public class AccountService {
  @Resource
  private AccountDAO accountDAO;
  @Resource
  private RetryTemplate retryTemplate;

  @Transactional
  public Account update(Account account) {
    return retryTemplate.execute(context -> {
      if (account.getId() == 0) {
        System.out.println("update retry...");
        throw new InvalidParameterException("Invalid parameter");
      }
      return accountDAO.saveAndFlush(account);
    }, context -> {
      System.out.println("retry finished...");
      return null;
    });
  }
}</code>

Test case for programmatic retry

<code>@Test
public void testUpdate() {
  Account account = new Account();
  account.setId(0L);
  account.setMoney(BigDecimal.valueOf(1000));
  account.setUserId("1");
  accountService.update(account);
}</code>

Execution result shows three retry attempts followed by the fallback message.

<code>update retry...
update retry...
update retry...
retry finished...</code>

RetryContext and Recovery Callback

<code>private static int index = 0;

public static String save() {
  logger.info("Executing save task...");
  System.out.println(1 / 0); // will throw
  return "success";
}

RetryTemplate template = RetryTemplate.builder()
    .maxAttempts(3)
    .retryOn(Exception.class)
    .customBackoff(BackOffPolicyBuilder.newBuilder().delay(100).multiplier(3).build())
    .build();

String result = template.execute(context -> {
  context.setAttribute("c", index++);
  System.out.println(context.getAttribute("c"));
  return save();
}, new RecoveryCallback<String>() {
  @Override
  public String recover(RetryContext context) throws Exception {
    return "Default system value";
  }
});
System.out.println("Result: " + result);
</code>

Execution logs show three attempts and the final recovered result.

<code>0
... INFO - Executing save task...
1
... INFO - Executing save task...
2
... INFO - Executing save task...
Result: Default system value</code>

The article concludes with a hope that the content is helpful.

End of article
End of article
End illustration
End illustration
JavaBackend DevelopmentSpringBootRetryableSpring Retry
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.