Spring Retryable
You might often encounter various errors and failures in the applications as the software world is highly unpredictable with variables ranging from network latency to third-party service downtime. Therefore it is crucial to build a robust application by ensuring fault tolerance and resilience that will help to build overall user experience and stability of the application.
Spring framework provides @Retryable
annotation that automates the retries for methods that might fail due to transient failures. A transient failure is generally a failure that occurs temporarily and can be resolved by retrying the operation.
Need for Retry
Let’s say your service retrieves data from remote API and it establishes an HTTP request and data is returned to the service. But what if the remote server is under heavy load, your own service is experiencing network latency, or any number of other transient problems. If your application doesn’t handle these scenarios well, you end up with failed operations and you lost the business.
Retrying an operation may seem to be an obvious solution, but implementing it manually in your code could lead to hard-to-maintain code. Let’s take the following example:
public class EmployeeService {
public List<Employee> getEmployeeList() {
int maxAttempts = 0;
while(maxAttempts < 3) {
try {
List<Employee> employees = // external API call to fetch employee details
return employees;
}catch(Exception ex) {
maxAttempts++;
}
}
//throw an exception
}
}
In the above code snippets you see that I have implemented retry logic manually using while loop and eventually it would become difficult to manage the code as you add more retry features like varying intervals, different kind of exceptions or multiple exceptions, etc.
Configure Retry
Spring framework provides simple process with @Retryable
annotation and using this annotation Spring framework provides declarative way of adding retry logic directly into the components. Thus it eliminates the need for boilerplate code and for example, it may look like the following code example:
public class EmployeeService {
@Retryable(retryFor = Exception.class)
public List<Employee> getEmployeeList() {
List<Employee> employees = // external API call to fetch employee details
return employees;
}
}
With the above @Retryable
annotation Spring will automatically retry the method getEmployeeList()
if it throws Exception
. The default maximum attempts for retry is 3 times and you can customize the maximum attempts and even you can pass other parameters as per your requirements.
Spring framework provides different types of retry policy that can be used as per your application’s requirements.
The basic or simple retry policy that retries the operation for a fixed number of times at a fixed interval or delay. This retry policy can be configured as shown in the following example:
public class EmployeeService {
@Retryable(retryFor = Exception.class, maxAttempts = 4, backoff = @Backoff(delay = 1000))
public List<Employee> getEmployeeList() {
List<Employee> employees = // external API call to fetch employee details
return employees;
}
}
In the above example, I have configured the annotation @Retryable
on the method getEmployeeList()
and it indicates that the method should be retried if any Exception
occurs. So the method should be retried for maximum 4 attempts with a delay of 1000 milliseconds (1 second) between each retry.
Another retry policy called exponential backoff retry polciy can be applied for your application and it increases the delay between each retry exponentially. This may allow the system to recover from failures more gracefully. For example, the following configuration can be done:
public class EmployeeService {
@Retryable(retryFor = Exception.class, maxAttempts = 5, backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000))
public List<Employee> getEmployeeList() {
List<Employee> employees = // external API call to fetch employee details
return employees;
}
}
In the above example I have configured maximum attempts 5 and I have specified multiplier 2, i.e., it will double the delay after each retry. But the maximum delay will not exceed 10000 milliseconds (10 seconds) as the maxDelay
specified is 10000 milliseconds.
You can also configure the random backoff retry policy that introduces some randomness in the delay between retries. This type of configuration is useful when the multiple instances of the same application are running and you want to avoid to retry the operation at the same time. You need to configure the delay and maximum delay for this random policy. For example:
public class EmployeeService {
@Retryable(retryFor = Exception.class, maxAttempts = 4, backoff = @Backoff(delay = 1000, maxDelay = 10000, random = true))
public List<Employee> getEmployeeList() {
List<Employee> employees = // external API call to fetch employee details
return employees;
}
}
In the above example, the randomness (random = true
) between each retry has been introduced. The random delay will be determined between the delay
(1 second) and maxDelay
(10 seconds) time.
If none of the above retry policy fits your requirement then you can create your own retry policy and it is called custom retry policy. For example, the below code snippets shows how to create a custom retry policy:
public class CustomRetryPolicy implements RetryPolicy {
private static final long serialVersionUID = 1L;
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) {
}
@Override
public void registerThrowable(RetryContext context, Throwable throwable) {
RetryContextSupport retryContext = (RetryContextSupport) context;
retryContext.registerThrowable(throwable);
}
}
This above class implements the Spring’s 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 used to initialize a new retry context for each retry attempt.
The close()
method is overridden, but left empty as it does not need any functionality to be added for 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’s Retry framework to track the exceptions and decide if another retry attempt should be made or not.
The following code snippets configures the custom retry policy.
@Configuration
public class AppConfig {
@Value("${retry.maxAttempts:3}")
private int maxAttempts;
@Value("${retry.backoffInMillis:1000}")
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;
}
}
A RetryTemplate
bean with a custom retry and backoff policy has been configured in the application configuration class.
The RetryTemplate
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 and backoff policy.
The @Bean
annotated retryTemplate()
method returns a RetryTemplate
instance with a CustomRetryPolicy
and a FixedBackOffPolicy
. The CustomRetryPolicy
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 the retriable operations by wrapping them in a retryTemplate.execute()
call.
Next how you are going to use the custom retry policy is given in the following example:
public class EmployeeService {
@Autowired
private RetryTemplate retryTemplate;
public List<Employee> getEmployeeList() {
retryTemplate.execute((RetryCallback<List<Employee>, RestClientException>) context -> {
List<Employee> employees = // external API call to fetch employee details
return employees;
});
}
}
The above code uses retryTemplate
instance to execute the retriable operation of the retrieving the list of employee details from an external API.
Spring’s Recover
Spring framework also provides a recover method to recover from the failed operation. The recover method gets called when all attempts to retry operations have been failed and the operation has not still been succeeded.
The recover method has the same signature as the method is being retried, except that it has an additional parameter of the Throwable type to receive the exception that causes the operation to fail. Here is an example of retry with recover:
public class EmployeeService {
@Autowired
private RestTemplate restTemplate;
@Retryable(retryFor = Exception.class, maxAttempts = 4, backoff = @Backoff(delay = 1000, maxDelay = 10000, random = true))
public List<Employee> getEmployeeList() {
System.out.println("Try ");
List<Employee> employees = // external API call to fetch employee details
return employees;
}
@Recover
public List<Employee> employees(Exception e) {
List<Employee> employees = Arrays.asList(new Employee(1, "Smith John", 1000000.00),
new Employee(2, "John Carle", 1200000.00));
return employees;
}
}
In the above code snippets notice the retryFor
has the Exception
class, so the recover method also has the Exception
as an argument.
The Spring framework provides @Retryable
and @Recover
annotations for retry logic for your applications. Though @Retyable
and @Recover
come with a rich set of customizable options, it’s important to use them judiciously, keeping both their use-cases and limitations in mind.