Retry configuration and gotchas

Learn about this workaround for retry configurations in Spring applications.
February 09, 2023
Last updated February 09, 2023

You can configure retries in Spring applications several different ways. Two popular ways to configure retries are using Spring Retry and Resilience4j Spring Boot starter. While these are popular, I ran into some limitations with using these tools. This tutorial explains those limitations and how you can work around them.

Limitation of Spring Retry and Spring Boot Resilience4j

A limitation I observed with Spring Retry and Spring Boot Resilience4j is that they work if the method you want to retry is at the top level of the class.

For example, it works for an @Controller annotated class A to call a method B1 in @Service annotated class B. However, it does not work to retry a method B2 that was called from B1.

The reason for this is that Spring-retry uses the @Retryable annotation to automatically re-invoke a failed operation. @Retryable is implemented using Spring AOP which means only external calls to retriable methods go through the proxy. Internal calls within the class bypass the proxy and, therefore, are not retried.

Steps to add the Retry capability using Resilience4j

  1. Add the resilience4j dependency in build.gradle, like so:

    1implementation "io.github.resilience4j:resilience4j-
    retry:${resilience4jVersion}" 
    
  2. Define the Retry bean in the application configuration.

The following configuration attempts a retry three times including the initial attempt. It attempts retry just for ObjectOptimisticLockingFailureException. It will wait for 300 milli seconds between the retries. If all retries are exhausted, an error is thrown.

   @Bean
   public Retry retry() {
     RetryConfig config = RetryConfig.custom()
              .maxAttempts(3)
              .waitDuration(Duration.ofMillis(300))
   	           .retryExceptions(ObjectOptimisticLockingFailureException.class)
                .failAfterMaxAttempts(true)
   	           .build();

         // Create a RetryRegistry with a custom global configuration
         RetryRegistry registry = RetryRegistry.of(config);
         return registry.retry( name:”dbRetry”);
      }
   }
  1. Define the Java 8 function for inner method retryWith. For example, this ::retryWith.

  2. The function defined in the code listing above can be decorated with Retry configured as below.

    this.retryableFunction = Retry.decorateFunction(retry, this::retryWith);
    
  3. Call the Retryable method as shown below:

    ScanRequest scanRequest = ScanRequest.builder()
        .entity(newScanRecordEntity)
        .eventType(scanCallbackRequest.getEventType())
        .build();
    //Try updating DB with retry.
    ScanRecordEntity scanRecordEntity = retryableFunction.apply(scanRequest);
    

Conclusion

Using these steps above, I was able to avoid refactoring my code. More importantly, I did not have to change any tests but were still able to achieve the retry mechanism as intended. I hope this helps you save time in troubleshooting and implementing retry for inner methods.

© 2023 Discover Financial Services. Opinions are those of the individual author. Unless noted otherwise in this post, Discover is not affiliated with, nor endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are property of their respective owners