DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Graceful Shutdown: Spring Framework vs Golang Web Services
  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4
  • Structured Logging in Spring Boot 3.4 for Improved Logs
  • How Stalactite ORM Implements Its Fluent DSL

Trending

  • How to Introduce a New API Quickly Using Micronaut
  • Useful System Table Queries in Relational Databases
  • Introducing Graph Concepts in Java With Eclipse JNoSQL, Part 2: Understanding Neo4j
  • Monoliths, REST, and Spring Boot Sidecars: A Real Modernization Playbook
  1. DZone
  2. Coding
  3. Frameworks
  4. Implementing Exponential Backoff With Spring Retry

Implementing Exponential Backoff With Spring Retry

Optimize Java retries with Spring Retry. Replace manual loops with @Retryable, add exponential backoff, and manage API, DB, and messaging failures easily.

By 
Danil Temnikov user avatar
Danil Temnikov
DZone Core CORE ·
Feb. 06, 25 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
3.1K Views

Join the DZone community and get the full member experience.

Join For Free

Hi, engineers! Have you ever been asked to implement a retry algorithm for your Java code? Or maybe you saw something similar in the codebase of your project?

Java
 
public void someActionWithRetries() {
  int maxRetries = 3;
  int attempt = 0;    
  while (true) {
    attempt++; 
    try {
      System.out.println("attempt number = " + attempt);
      performTask();
      System.out.println("Task completed");
      break; 
    } catch (Exception e) {
      System.out.println("Failure: " + e.getMessage());
      if (attempt >= maxRetries) {
        System.out.println("Max retries attempt”);
        throw new RuntimeException("Unable to complete task after " + maxRetries + " attempts", e);
      }
      System.out.println("Retrying");
    }
  }
}


We can see that the code above executes a while loop until a task is successfully performed or the maximum retry attempt is reached. In this scenario, an exception is thrown, and the method execution terminates.

But what if I tell you that this code might be wrapped into one line method with one annotation? 

This is the moment Spring Retry enters the room.

Let’s first answer this simple question: When do we need retries?  

  1. API integration. Our downstream service might be unavailable for short periods or just throttling, and we want to retry in case of any of these scenarios.
  2. DB connection. DB Transaction may fail because of, for instance, replicas switching or just because of a short time peak in db load. We want to implement retries to back up these scenarios as well.  
  3. Messaging processing. We want to make sure that when we are consuming messages our service will not fail processing messages in case of first error. Our goal is to give a second chance before sending a message to a dead letter queue.

Implementing Spring Retry

To add Spring Retry to your application, you need to add two dependencies first: Spring Retry and Spring AOP. As of writing this article, the versions below are the latest. 

XML
 
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>2.0.11</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.2.2</version>
</dependency>


We also need to enable retries using annotation @EnableRetry. I’m adding this annotation above @SpringBootApplication annotation.

Java
 
@EnableRetry
@SpringBootApplication
public class RetryApplication {
  public static void main(String[] args) {
     SpringApplication.run(RetryApplication.class, args);
  }
}


Remember the code I started with? Let’s create a new service and put this code into it. Also, let’s add an implementation of the performTask method, which typically throws exceptions.

Java
 
@Service
public class RetriableService {

   public void someActionWithRetries() {
       int maxRetries = 3;
       int attempt = 0;
       while (true) {
           attempt++;
           try {
               System.out.println("attempt number = " + attempt);
               performTask();
               System.out.println("Task completed");
               break;
           } catch (Exception e) {
               System.out.println("Failure: " + e.getMessage());
               if (attempt >= maxRetries) {
                   System.out.println("Max retries attempt");
                   throw new RuntimeException("Unable to complete task after " + maxRetries + " attempts", e);
               }
               System.out.println("Retrying");
           }
       }
   }

   private static void performTask() throws RuntimeException {
       double random = Math.random();
       System.out.println("Random =" + random);
       if (random < 0.9) {
           throw new RuntimeException("Random Exception");
       }
       System.out.println("Exception was not thrown");
   }
}


And let’s add this service execution to our application entry point.

Java
 
@EnableRetry
@SpringBootApplication
public class RetryApplication {
   public static void main(String[] args) {
       ConfigurableApplicationContext ctx = SpringApplication.run(RetryApplication.class, args);
       RetriableService bean = ctx.getBean(RetriableService.class);
       bean.someActionWithRetries();
   }
}


Our main goal is to execute performTask without exceptions. We implemented a simple retry strategy using a while loop and manually managing the number of retries and behavior in case of errors. 

Additionally, we updated our main method, just to make code executable (you may execute it any way you prefer to, it actually does not matter).

When we run our application, we may see a similar log:

Plain Text
 
attempt number = 1
Random =0.2026321848196292
Failure: Random Exception
Retrying
attempt number = 2
Random =0.28573469016365216
Failure: Random Exception
Retrying
attempt number = 3
Random =0.25888484319397653
Failure: Random Exception
Max retries attempt
Exception in thread "main" java.lang.RuntimeException: Unable to complete task after 3 attempts


As we can see, we tried the times and threw an exception when all three attempts failed. Our application is working, but just to execute a one-line method, we added a lot of lines of code. And what if we want to cover another method with the same retry mechanism? Do we need to copy-paste our code? We see that even though our solution is a working one, it does not seem to be an optimal one. 

Is there a way to make it better?

Yes, there is. We are going to add Spring Retry to our logic. We’ve already added all necessary dependencies and enabled Retry by adding annotation. Now, let’s make our method retriable using Spring Retry.

We just need to add the following annotation and provide the number of maximum attempts:

Plain Text
 
@Retryable(maxAttempts = 3)


In the second step, we need to delete all the useless code, and this is how our service looks now:

Java
 
@Retryable(maxAttempts = 3)
public void someActionWithRetries() {
   performTask();
}



private static void performTask() throws RuntimeException {
   double random = Math.random();
   System.out.println("Random =" + random);
   if (random < 0.9) {
       throw new RuntimeException("Random Exception");

   }
   System.out.println("Exception was not thrown");
}


When we execute our code, we will see the following log line:

Plain Text
 
Random =0.04263677120823861
Random =0.6175610369948504
Random =0.226853770441114
Exception in thread "main" java.lang.RuntimeException: Random Exception


The code is still trying to perform the task and fails after three unsuccessful attempts. We have already improved our code by adding an aspect to handle retries. But, we also can make our retries more efficient by introducing an exponential backoff strategy.

What Is Exponential Backoff?

Imagine you are calling an external API for which you have some quotas. Sometimes, when you reach your quota, this API throws a throttling exception saying your quota has been exceeded and you need to wait some time to be able to make an API call again.

You know that quotas should be reset quite soon, but I don’t know when exactly it will happen. So, you decide to keep making API calls until a successful response is received, but increase the delay for every next call.

For instance:

Plain Text
 
1st call - failure
Wait 100ms 
2nd call - failure
Wait 200ms
3rd call - failure
Wait 400ms
4th call - success


You can see how delays between calls are exponentially increasing. This is exactly what the exponential backoff strategy is about -> retying with exponentially increasing delay. 

And yes, we can simply implement this strategy using Spring Retry. Let’s extend our code:

Java
 
@Retryable(maxAttempts = 10, backoff = @Backoff(delay = 100, multiplier = 2.0, maxDelay = 1000))
public void someActionWithRetries() {
   performTask();
}


We’ve increased maxAttempts value to 10, and added backoff configuration with the following params:

  1. Delay – delay in milliseconds for the first retry

  2. Multiplier – multiplier for 2nd and following retry. In our case, the second retry will happen in 200ms after the 1st retry failed. If the second retry fails, the third will be executed in 400ms, etc.

  3. maxDelay – the limit in milliseconds for delays. When your delay reaches the maxDelay value, it is not increasing anymore.

Let’s add one more log line to be able to track milliseconds of the current timestamp in the performTask method and execute our code:

Java
 
private static void performTask() throws RuntimeException {
	System.out.println("Current timestamp=" +System.currentTimeMillis()%100000);
  	........
}


Plain Text
 
Current timestamp=41935
Random =0.5630325878313412
Current timestamp=42046
Random =0.3049870877017091
Current timestamp=42252
Random =0.6046786246149355
Current timestamp=42658
Random =0.35486866685708773
Current timestamp=43463
Random =0.5374704153455458
Current timestamp=44469
Random =0.922956819951388
Exception was not thrown


We can see that it took six attempts (five retries) to perform the task without an exception. We can also see that the difference between the first and second execution is about 100 ms, as configured.

The difference between the second and third execution is about 200 ms, confirming that the multiplier of 2 is working as expected.

Pay attention to the delay before the last execution. It is not 1,600 ms, as we might have expected (a multiplier of 2 for the fifth execution), but 1,000 ms because we set that as the upper limit.

Conclusion

We successfully implemented an exponential backoff strategy using Spring Retry. It helped us to get rid of Utils code and make our retry strategy more manageable. We also discussed scenarios when retries are mostly used, and now we are more aware of when to use them.

The functionality I showed in this article is only about 30% of what Spring Retry allows us to do, and we will see more advanced approaches in the next article.

Exponential backoff Spring Framework Framework

Opinions expressed by DZone contributors are their own.

Related

  • Graceful Shutdown: Spring Framework vs Golang Web Services
  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4
  • Structured Logging in Spring Boot 3.4 for Improved Logs
  • How Stalactite ORM Implements Its Fluent DSL

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!