Spring Boot Retry

Spring Boot Retry: How to Handle Errors and Retries in Your Application

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 Retry

In 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 Retry

In 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 Retry

In 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 Policy

This 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 Call

This 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 Config

This 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 Method

In 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 Type

In 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.xml

The 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 .java

This 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 Example

In 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

Your email address will not be published.