Spring REST API Retryable And Recover

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.

Source Code

Download

Leave a Reply

Your email address will not be published. Required fields are marked *