Christopher Anabo
Christopher Anabo
Senior Tech Lead
Christopher Anabo

Notes

Building Resilient Java Applications - Spring Retry

Building Resilient Java Applications - Spring Retry

Introduction

In today's world of microservices and distributed systems, handling failure gracefully and maintaining resiliency in applications is crucial. One powerful way to achieve this is by using the Spring Retry library, part of the broader Spring ecosystem. Spring Retry simplifies the process of incorporating retry logic into your Java applications. This article explores the basics of Spring Retry and demonstrates how to implement resilient Java applications using this library.

 

Getting Started with Spring Retry

To start using Spring Retry, you need to add the required dependency to your project. If you’re using Maven, include the following dependency in your pom.xml:

<dependency> <groupid>org.springframework.retry</groupid> <artifactid>spring-retry</artifactid> <version>1.3.1</version> </dependency>

 

Basic Retry Template

Spring Retry provides the RetryTemplate class, which is the core component for implementing retry logic. Here's a simple example demonstrating how to use RetryTemplate to retry an operation up to 3 times with a fixed delay of 1 second between attempts:

 

import org.springframework.retry.support.RetryTemplate;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;

public class BasicRetryExample {

    public static void main(String[] args) {
        RetryTemplate retryTemplate = new RetryTemplate();

        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(1000); // 1 second
        retryTemplate.setBackOffPolicy(backOffPolicy);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(3);
        retryTemplate.setRetryPolicy(retryPolicy);

        String result = retryTemplate.execute(context -> {
            System.out.println("Attempting...");
            return performOperation();
        });

        System.out.println("Result: " + result);
    }

    private static String performOperation() {
        // Your operation implementation here
    }
}

 

Recovering from Failures

In some cases, you may want to perform a recovery action if all retry attempts have failed. Spring Retry provides the RecoveryCallback interface for this purpose. Here's an example:

import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryCallback;

public class RecoveryExample {

    public static void main(String[] args) {
        RetryTemplate retryTemplate = createRetryTemplate();

        RetryCallback retryCallback = context -> {
            System.out.println("Attempting...");
            return performOperation();
        };

        RecoveryCallback recoveryCallback = context -> {
            System.out.println("Recovery action...");
            return "Recovered";
        };

        String result = retryTemplate.execute(retryCallback, recoveryCallback);
        System.out.println("Result: " + result);
    }

    // Other methods omitted for brevity
}

 

What if I want to implement custom retry policy

Custom Retry Policies

Spring Retry offers various retry policies for different scenarios. The ExpressionRetryPolicy allows you to define a custom retry policy using a SpEL (Spring Expression Language) expression. For example, retry the operation only if the exception message contains "transient":

Implementing a custom retry policy in Spring Retry can be valuable in several scenarios. Here are some use cases where a custom retry policy would be beneficial:

1. Handling Specific Exceptions

If your application needs to retry only specific types of exceptions or implement different retry logic based on the type of exception, a custom retry policy allows you to define these rules precisely.

2. Complex Backoff Strategies

Standard backoff strategies (like fixed delay or exponential backoff) may not always fit your requirements. A custom retry policy can implement more complex backoff strategies, such as logarithmic backoff, random jitter, or custom algorithms based on specific business logic.

3. Adaptive Retry Logic

For systems that need to adjust their retry behavior based on runtime conditions, such as the current load, time of day, or external factors, a custom retry policy can adapt the retry mechanism dynamically.

4. Rate Limiting

When interacting with external services that enforce rate limits, a custom retry policy can help manage retries in a way that respects these limits, preventing further throttling or blocking.

5. Contextual Retries

In scenarios where retries need to take into account the context of the operation (e.g., user-specific data, transactional state), a custom retry policy can incorporate this contextual information into its decision-making process.

6. Transaction Integrity

For applications that need to maintain transaction integrity, custom retry policies can ensure that retries are managed in a way that does not violate transactional constraints or lead to data inconsistencies.

Example: Implementing a Custom Retry Policy

Here’s an example of how you might implement a custom retry policy in Spring Retry:

1. Define the custom retry policy

import org.springframework.retry.RetryContext;
import org.springframework.retry.policy.SimpleRetryPolicy;

import java.util.HashMap;
import java.util.Map;

public class CustomRetryPolicy extends SimpleRetryPolicy {

    private Map, Boolean> retryableExceptions;

    public CustomRetryPolicy(int maxAttempts) {
        super(maxAttempts);
        this.retryableExceptions = new HashMap<>();
    }

    public void setRetryableExceptions(Map, Boolean> retryableExceptions) {
        this.retryableExceptions = retryableExceptions;
    }

    @Override
    public boolean canRetry(RetryContext context) {
        Throwable lastThrowable = context.getLastThrowable();
        if (lastThrowable != null && retryableExceptions.containsKey(lastThrowable.getClass())) {
            return retryableExceptions.get(lastThrowable.getClass());
        }
        return super.canRetry(context);
    }
}

2. Configure the retry template

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryOperations;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.support.RetryTemplate;

@Configuration
public class RetryConfig {

    @Bean
    public RetryOperations retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();

        CustomRetryPolicy retryPolicy = new CustomRetryPolicy(3);
        Map, Boolean> retryableExceptions = new HashMap<>();
        retryableExceptions.put(IOException.class, true);
        retryableExceptions.put(SQLException.class, false);
        retryPolicy.setRetryableExceptions(retryableExceptions);

        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(2000);

        retryTemplate.setRetryPolicy(retryPolicy);
        retryTemplate.setBackOffPolicy(backOffPolicy);

        return retryTemplate;
    }
}

3. Use the retry template

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryOperations;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Autowired
    private RetryOperations retryTemplate;

    public void performTask() {
        try {
            retryTemplate.execute((RetryCallback) context -> {
                // Your business logic here
                if (Math.random() > 0.5) {
                    throw new IOException("Simulated IOException");
                }
                return null;
            });
        } catch (Exception e) {
            System.out.println("Task failed after retries: " + e.getMessage());
        }
    }
}

2. Implementing in Springboot

a.  Enable Spring Retry: Annotate your main application class with @EnableRetry.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;

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

b. Create a Service with Retry Logic: Use the @Retryable annotation to define the retry logic.

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class RetryService {

    @Retryable(
      value = { Exception.class }, 
      maxAttempts = 3, 
      backoff = @Backoff(delay = 2000))
    public void performTask() throws Exception {
        System.out.println("Attempting to perform the task");
        if (Math.random() > 0.5) {
            throw new Exception("Simulated failure");
        }
        System.out.println("Task completed successfully");
    }

    @Recover
    public void recover(Exception e) {
        System.out.println("Recovering from failure: " + e.getMessage());
    }
}

c. Use the Service: Autowire the service in a controller or another component and call the method.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RetryController {

    @Autowired
    private RetryService retryService;

    @GetMapping("/retry")
    public String retryEndpoint() {
        try {
            retryService.performTask();
            return "Task completed successfully";
        } catch (Exception e) {
            return "Task failed after retries";
        }
    }
}

Notes

  • @EnableRetry: Enables Spring Retry functionality in your application.
  • @Retryable: Defines the retry logic. The value attribute specifies which exceptions to retry, maxAttempts specifies the number of retry attempts, and backoff defines the delay between retries.
  • @Recover: Defines the recovery logic to execute when the retries are exhausted.

This setup allows your application to retry the specified method up to three times with a 2-second delay between attempts if an exception is thrown. If all retries fail, the @Recover method is called.

 

Conclusion

Custom retry policies provide the flexibility needed to handle specific requirements and complex scenarios in a way that built-in policies may not support. By implementing a custom retry policy, you can tailor the retry mechanism to fit your application's unique needs, ensuring more robust and resilient error handling.