Discover the power of Spring Boot Retry mechanisms and learn how to use them to improve the robustness and resilience of your Spring Boot applications. Explore different types of retries and their configurations, with examples and best practices.
Introduction
As developers, we often encounter errors and failures in our applications due to various reasons such as network issues, server downtime, or timeouts. Handling such errors gracefully is crucial for the user experience and the overall stability of the application. In this blog post, we will discuss the retry mechanism in Spring Boot, which allows us to retry failed operations automatically.
What is Retry Mechanism?
Retry is a common mechanism used in distributed systems to handle transient failures. A transient failure is an error that occurs temporarily and can be resolved by retrying the operation. The retry mechanism retries the failed operation until it succeeds or reaches the maximum number of attempts.
What is Spring Boot Retry
Spring Boot Retry is a mechanism that allows Spring Boot applications to automatically retry failed operations. It provides a simple and flexible way to handle errors and failures by automatically retrying the failed operations with different configurations until they succeed or reach a maximum number of attempts. It allows you to focus on the business logic of your application without worrying about the underlying infrastructure and error handling.
Types of Retries in Spring Boot
Spring Boot Retry is designed to work with any Spring-managed bean and supports various retry policies, including fixed, exponential, and random delays between retries. It also allows developers to customize the backoff and retry policies and provides a way to recover from failures using a recovery callback method.
Spring Boot provides several types of retries that we can use based on our application’s requirements. Let’s explore them in detail:
1. Simple Retry – SimpleRetryPolicy
Simple Retry is the most basic retry mechanism provided by Spring Boot. It retries the operation a fixed number of times with a fixed delay between each retry. We can configure the number of retries and delay using SimpleRetryPolicy.
Example:
@Retryable(retryFor = { Exception.class },
maxAttempts = 3,
backoff = @Backoff(delay = 1000))
public void myMethod() {
// Perform some operation
}
Simple RetryIn the above example, we have annotated the method with @Retryable, which indicates that this method should be retried if an exception is thrown. We have also configured the maximum number of attempts to 3 and a delay of 1000 milliseconds (1 second) between each retry.
2. Exponential Backoff Retry – ExponentialBackOffPolicy
Exponential Backoff Retry is an enhanced retry mechanism that increases the delay between each retry exponentially. This allows the system to recover from failures more gracefully. We can configure the initial delay, multiplier, and maximum delay using ExponentialBackOffPolicy.
Example:
@Retryable(retryFor = { Exception.class },
maxAttempts = 5,
backoff = @Backoff(delay = 2000, multiplier = 2, maxDelay = 10000))
public void myMethod() {
// Perform some operation
}
Exponential Backoff RetryIn the above example, we have configured the maximum number of attempts to 5 and an initial delay of 2000 milliseconds (2 seconds). We have also specified a multiplier of 2, which means that the delay will be doubled after each retry. The maximum delay is set to 10000 milliseconds (10 seconds).
3. Random Backoff Retry – RandomBackOffPolicy
Random Backoff Retry is a retry mechanism that introduces some randomness in the delay between retries. This is useful when multiple instances of the same application are running, and we want to avoid retrying at the same time. We can configure the minimum and maximum delay using RandomBackOffPolicy.
Example:
@Retryable(retryFor = { Exception.class },
maxAttempts = 3,
backoff = @Backoff(delay = 5000, maxDelay = 10000, random = true))
public void myMethod() {
// Perform some operation
}
Random Backoff RetryIn the above example, we have configured the maximum number of attempts to 3 and a delay of 500 milliseconds (5 seconds) for the first retry. We have also set the maximum delay to 10000 milliseconds (10 seconds) and enabled randomness in the delay between retries.
4. Custom Retry – CustomRetryPolicy
If none of the built-in retry mechanisms meet our requirements, we can create a custom retry policy. We can implement the RetryPolicy interface and provide our own logic for deciding whether to retry the operation or not.
Example:
package com.bootcamptoprod.retry.retry;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.context.RetryContextSupport;
public class CustomRetryPolicy implements RetryPolicy {
private int maxAttempts;
public CustomRetryPolicy(int maxAttempts) {
this.maxAttempts = maxAttempts;
}
@Override
public boolean canRetry(RetryContext context) {
int attempts = context.getRetryCount();
return attempts < maxAttempts;
}
@Override
public RetryContext open(RetryContext parent) {
return new RetryContextSupport(parent);
}
@Override
public void close(RetryContext context) {
// Do nothing
}
@Override
public void registerThrowable(RetryContext context, Throwable throwable) {
RetryContextSupport retryContext = (RetryContextSupport) context;
retryContext.registerThrowable(throwable);
}
@Override
public String toString() {
return "CustomRetryPolicy [maxAttempts=" + maxAttempts + "]";
}
}
Custom Retry PolicyThis code is an implementation of the Spring Retry RetryPolicy
interface to provide a custom retry policy for failed operations.
The CustomRetryPolicy
constructor takes an integer value maxAttempts
as the maximum number of times the operation can be retried. The canRetry
method is overridden to check if the number of retry attempts made so far is less than the maxAttempts
value. If it is, then the method returns true
indicating that the operation can be retried. If the number of retry attempts exceeds the maxAttempts
value, the method returns false
, indicating that the operation should not be retried.
The open
method is overridden to return a new instance of the RetryContextSupport
class. This is done to initialize a new retry context for each retry attempt.
The close
method is overridden, but left empty as it does not require any functionality in this example.
The registerThrowable
method is overridden to register the exception thrown during the operation’s retry attempt with the retry context. This allows the Spring Retry framework to track the exceptions and decide if another retry attempt should be made.
Finally, the toString
method is overridden to return a string representation of the CustomRetryPolicy
class, which includes the maxAttempts
value.
public Movie getMovieDetails(String movieId) {
return retryTemplate.execute((RetryCallback<Movie, RestClientException>) context -> {
Movie movie = null;
try {
movie = movieApiClient.getMovieDetails(movieId);
} catch (HttpServerErrorException httpServerErrorException) {
throw httpServerErrorException;
} catch (HttpClientErrorException httpClientErrorException) {
throw httpClientErrorException;
} catch (ResourceAccessException resourceAccessException) {
throw resourceAccessException;
}
return movie;
});
}
API CallThis code is a method that uses Spring RetryTemplate to execute a retryable operation of fetching movie details by movie ID from an external API.
The method takes a movieId parameter and returns a Movie object. It uses the execute method of RetryTemplate and passes a RetryCallback as a parameter to the execute method. The RetryCallback has two generic parameters: the return type of the retryable operation (Movie), and the exception type that may be thrown (RestClientException).
Within the RetryCallback’s doWithRetry method, a Movie object is assigned a null value. Then, a try-catch block is used to attempt to fetch the movie details by movie ID from an external API via the movieApiClient. If the movie details cannot be fetched due to an HTTP server error, an HTTP client error, or a resource access exception, an appropriate exception is thrown, which will be caught by the RetryTemplate and retried according to the retry policy.
The method returns the movie object fetched from the external API or null if the retry operation fails.
import com.bootcamptoprod.retry.retry.CustomRetryPolicy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.support.RetryTemplate;
@Configuration
public class RetryConfig {
@Value("${retry.maxAttempts:3}")
private int maxAttempts;
@Value("${retry.backoffInMillis:2000}")
private long backoffInMillis;
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(backoffInMillis);
retryTemplate.setBackOffPolicy(backOffPolicy);
CustomRetryPolicy retryPolicy = new CustomRetryPolicy(maxAttempts);
retryTemplate.setRetryPolicy(retryPolicy);
return retryTemplate;
}
}
Retry ConfigThis code is a configuration class that defines a RetryTemplate
bean with a custom retry policy and backoff policy.
The RetryTemplate
is a Spring class that provides a way to execute operations that may fail and can be retried in case of failure. It allows customizing the retry behavior through a retry policy and a backoff policy.
The retryTemplate
method annotated with @Bean
creates and returns a RetryTemplate
instance with a CustomRetryPolicy
and a FixedBackOffPolicy
. The CustomRetryPolicy
is a custom retry policy that is defined in a separate class and takes the maxAttempts
parameter from the application configuration. The FixedBackOffPolicy
is a backoff policy that sets a fixed time interval between retries and takes the backoffInMillis
parameter from the application configuration.
Finally, the RetryTemplate
instance is returned and can be used to execute retryable operations by wrapping them in a retryTemplate.execute()
call.
Recover Method
In addition to retrying a failed operation, Spring Boot also provides a way to recover from the failure using the recover method. The recover method is called when all retry attempts have failed, and the operation has still not succeeded.
The recover method should have the same signature as the method being retried, except that it should have an additional parameter of the Throwable type to receive the exception that caused the operation to fail.
Example:
@Retryable(value = { Exception.class },
maxAttempts = 3,
backoff = @Backoff(delay = 5000))
public void myMethod() {
// Perform some operation
}
@Recover
public void recoverMethod(Exception e) {
// Handle the exception and return some default response in case of non-void methods
}
Recover MethodIn the above example, we have annotated the method with @Retryable
and specified the maximum number of attempts and the delay between retries. We have also defined a recover method using the @Recover
annotation, which takes an Exception parameter to handle the exception that caused the operation to fail.
When all retry attempts fail, the recoverMethod
will be called with the exception that caused the failure as the parameter. We can then handle the exception appropriately, such as logging the error, sending an alert, or taking some other corrective action.
Note that the recover method must have the same signature as the method being retried. If the method being retried has a return value, the recover method should also have the same return type.
Example with return value:
@Retryable(value = { Exception.class },
maxAttempts = 3,
backoff = @Backoff(delay = 5000))
public String myMethod() {
// Perform some operation
}
@Recover
public String recoverMethod(Exception e) {
// Handle the exception and return some default value
return "recovery-value";
}
Recover Method with Return TypeIn this example, both the myMethod and the recoverMethod have a return type of String.
Spring Boot Retry Example
Let’s say we have a movie recommendation service that relies on an external movie database API to fetch information about movies. Sometimes, due to network issues or temporary outages, the API may not be available, and our service may fail to fetch the data. In such cases, we can use the retry and recover mechanisms to automatically retry the operation and recover from failures.
Here’s how we can implement this in our movie recommendation service:
First of all, add the below-mentioned dependencies in pom.xml of the application.
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
pom.xmlThe first dependency org.springframework.retry:spring-retry
provides support for retrying operations that may fail due to transient faults or errors. It offers a set of configurable retry templates, policies, and backoff strategies to retry failed operations automatically.
The second dependency org.springframework:spring-aspects
provides Aspect-Oriented Programming (AOP) support in Spring. It includes the necessary classes and APIs to enable AOP in Spring applications, which can be used with the retry support to intercept and retry method invocations automatically.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;
@EnableRetry
@SpringBootApplication
public class SpringBootRetryApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootRetryApplication.class, args);
}
}
SpringBootRetryApplication .javaThis code represents the main entry point of a Spring Boot application with retry enabled. The @SpringBootApplication
annotation enables Spring Boot auto-configuration and component scanning. The @EnableRetry
annotation enables the Spring Retry framework to provide retry capabilities to any method in the application where the @Retryable
annotation is used.
import com.bootcamptoprod.retry.entity.Movie;
import com.bootcamptoprod.retry.rest.client.MovieApiClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClientException;
@Service
public class MovieService {
@Autowired
private MovieApiClient movieApiClient;
@Retryable(
retryFor = {HttpServerErrorException.class, HttpClientErrorException.class, ResourceAccessException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 5000))
public Movie getMovieDetails(String movieId) throws ResourceAccessException {
Movie movie = null;
try {
movie = movieApiClient.getMovieDetails(movieId);
} catch (HttpServerErrorException httpServerErrorException) {
System.out.println("Received HTTP server error exception while fetching the movie details. Error Message: " + httpServerErrorException.getMessage());
throw httpServerErrorException;
} catch (HttpClientErrorException httpClientErrorException) {
System.out.println("Received HTTP client error exception while fetching the movie details. Error Message: " + httpClientErrorException.getMessage());
throw httpClientErrorException;
} catch (ResourceAccessException resourceAccessException) {
System.out.println("Received Resource Access exception while fetching the movie details.");
throw resourceAccessException;
}
return movie;
}
@Recover
public Movie recoverMovieDetails(RestClientException e) {
// Log the error
System.out.println("Error fetching movie details: " + e.getMessage());
// Return a default movie
return new Movie("0000", "Default movie", "Unknown", 0.0);
}
}
Retryable & Recover ExampleIn this example, we have a MovieService
that fetches movie details from an external movie API using the MovieApiClient
class. We have annotated the getMovieDetails
method with @Retryable
and specified that it should be retried up to 3 times if a HttpServerErrorException
or HttpClientErrorException
or ResourceAccessException
is thrown (which can occur when there is a network issue or the API is not available due to server or client errors). We have also used the @Backoff
annotation to specify a delay of 5 seconds between retry attempts.
If all retry attempts fail, the recoverMovieDetails
method will be called with the exception that caused the failure. In this method, we can log the error and return a default movie to the caller.
So, if the movie API is not available when the getMovieDetails
method is called, the operation will be retried up to 3 times with a delay of 5 seconds between retry attempts. If all retry attempts fail, the recoverMovieDetails
method will be called, which will log the error and return a default movie to the caller. In recoverMovieDetails
method signature, we have specified the parent exception of HttpServerErrorException
, HttpClientErrorException
and ResourceAccessException
class. You can specify the exact same exception as well which you want to handle gracefully.
This is just one example of how retry and recover can be used in a real-life scenario, specifically in a movie recommendation system. By using these mechanisms, we can make our recommendation system more resilient to failures and provide a better user experience to our users.
Note:
There are some more classes that are not mentioned but are required for this example to work correctly like MovieApiClient, MovieController, MovieEntity and so on. For complete code, you can refer to source code section.
Output:
In the below screenshots, you can see the retry operation was performed three times, and after the failed attempts the response was returned from the recover method.
Source Code
The complete source code of the above example can be found here.
Also, the source code of Spring Retry using a custom retry policy can be found here.
FAQs
What are the types of retries in Spring Boot?
Spring Boot provides several types of retries, including Simple Retry, Exponential Backoff Retry, Random Backoff Retry, and Custom Retry.
What is the purpose of the @Retryable annotation?
The @Retryable annotation is used to mark a method as retryable. When the method throws an exception, Spring Retry will automatically attempt to retry the method according to the configured retry policy.
What is the purpose of the @Recover annotation?
The @Recover annotation is used to mark a method as a recovery method. If a @Retryable method throws an exception that exceeds the maximum number of retries, Spring Retry will call the @Recover method to handle the failure.
Can @Recover methods have parameters?
Yes, @Recover methods can have parameters. The parameters can be used to provide information about the failed operation, such as the original method’s parameters or the exception that was thrown.
How can I customize the retry behavior in Spring Retry?
The retry behavior can be customized by implementing a RetryPolicy, which determines when a retry should be attempted, and a BackOffPolicy, which determines how long to wait between retries.
Can we have multiple recover methods in the same class?
Yes, we can have multiple @Recover
methods in the same class, as long as each method has a different set of exception types specified in its @Recover
annotation. The Spring Retry framework will look for the appropriate @Recover
method based on the exception type thrown by the @Retryable
method.
Things to Consider
Here are some things to consider when working with Spring retry:
- Choosing the Right RetryPolicy: It is important to choose the appropriate RetryPolicy based on the use case, as it determines how the retries are attempted.
- Configuring the Backoff Policy: The Backoff Policy determines the wait time between retries. It is important to set the appropriate values for the initial backoff, max backoff, and backoff multiplier to prevent overloading the system.
- Choosing the right Recovery mechanism: Depending on the application’s use case, it is essential to choose the right Recovery mechanism to handle the retries.
- Monitoring retry attempts: Monitoring retry attempts and failures can help identify the root cause of the problem and improve the system’s resilience.
- Impact on performance: It is essential to understand the impact of retries on the system’s performance, and to avoid excessive retries, which can overload the system and lead to performance degradation.
- Logging and debugging: Proper logging and debugging can help to identify and diagnose issues related to retry and recovery mechanisms. It is important to log all the relevant information to help troubleshoot the issue.
Conclusion
Retry mechanism is a powerful feature in Spring Boot that allows us to handle errors and failures gracefully. In this post, we have explored different types of retries and their configuration in Spring Boot. We have also provided examples and tips for using retries effectively in our applications. By using the retry mechanism, we can improve the user experience and the overall stability of our application.
Learn More
Interested in learning more?
Check out our blog on how to use Embedded MongoDB database for building simple CRUD based applications.
Add a Comment