Generic Java retry service

08-07-2023
source code

Introduction

In this blog I will show you how to create a generic retry service in Java. This retry service can be used with any operation that you expect to fail on first time and may work on subsequent calls.

Background

I was working with an http service that sometimes returns 504 status code but then would work on second call. I thought this is a good scenario to create a generic retry function in Java that can be used for such scenario.

Retry Service Interface

I create an interface for the service shown below.

public interface RetryService {
     T retryCall(Supplier callToRetry, int maxAttempts) throws AttemptsFailedExceptions;
     T retryCallReturnDefault(Supplier callToRetry, int maxAttempts, T defaultValue);
}

The retryCall function takes in a Supplier<T> with max number of attempts, this function will throw AttemptsFailedExceptions exception in case all attempts failed. You can program against this exception, for example gracefully handle the call. The retryCallReturnDefault on the other hand takes an additional parameter which is the default value to return if all attempts fail.

Retry Service Implementation

A sample implementation for the RetryService interface can be found below.

@Service
public class RetryServiceImpl implements RetryService {
    @Override
    public  T retryCall(Supplier callToRetry, int maxAttempts) throws AttemptsFailedExceptions {
        int attempts = maxAttempts > 0 ? maxAttempts : 3;
        int totalAttempts = 0;
        while (totalAttempts < attempts){
            try {
                return callToRetry.get();
            } catch (Exception exception){
                if(totalAttempts < attempts) {
                    totalAttempts++;
                    continue;
                }
                throw exception;
            }
        }
        throw new AttemptsFailedExceptions("All attempts to call service failed");
    }

    @Override
    public  T retryCallReturnDefault(Supplier callToRetry, int maxAttempts, T defaultValue) {
        int attempts = maxAttempts > 0 ? maxAttempts : 3;
        int totalAttempts = 0;
        while (totalAttempts < attempts){
            try {
                return callToRetry.get();
            } catch (Exception exception){
                if(totalAttempts < attempts) {
                    totalAttempts++;
                    continue;
                }
                return defaultValue;
            }
        }
        return defaultValue;
    }
}

The Supplier<T> interface provide the get method which will make the call. This makes the function generic because you can pass whatever service call you want as long as it implements the Supplier<T> interface.

The two functions implementation looks the same apart from when all attempts failed. retryCall will throw AttemptsFailedExceptions exception whereas retryCallReturnDefault will return the default value.

Unit Tests

I have included unit tests for both functions implementation below.

class RetryServiceImplTest {
    @Test
    public void retryCall_should_retry_max_attempts() throws AttemptsFailedExceptions {
        RetryServiceImpl retryService = new RetryServiceImpl();
        Supplier supplierMock = mock(Supplier.class);
        when(supplierMock.get())
                .thenThrow(new RuntimeException("First attempt failed"))
                .thenThrow(new RuntimeException("Second attempt failed"))
                .thenReturn(1);
        int result = retryService.retryCall(supplierMock, 3);
        verify(supplierMock, times(3)).get();
        assertEquals(1, result);
    }

    @Test
    public void retryCallReturnDefault_should_retry_max_attempts_then_return_default() throws AttemptsFailedExceptions {
        RetryServiceImpl retryService = new RetryServiceImpl();
        Supplier supplierMock = mock(Supplier.class);
        when(supplierMock.get())
                .thenThrow(new RuntimeException("First attempt failed"))
                .thenThrow(new RuntimeException("Second attempt failed"))
                .thenThrow(new RuntimeException("Third attempt failed"));
        int result = retryService.retryCallReturnDefault(supplierMock, 3,2);
        verify(supplierMock, times(3)).get();
        assertEquals(2, result);
    }
}

I am using Mockito to mock the Supplier<T> implementation.

Sample Usage

I have implemented a sample API call to jsonplaceholder todos api. I will not worry if this stops working as you will get the default value.

@RestController
public class SampleUsageController {
    private final RetryService retryService;

    public SampleUsageController(RetryService retryService) {
        this.retryService = retryService;
    }

    @GetMapping("/sample")
    public String getResult() {
        String apiUrl = "https://jsonplaceholder.typicode.com/todos/1";

        RestTemplate restTemplate = new RestTemplate();

        return retryService.retryCallReturnDefault(
                () -> restTemplate.getForObject(apiUrl, String.class), 3,"No result" );
    }
}

This is SpringBoot RestController with one method that can be accessed using http://localhost:8080/sample. I am using the RetryService.retryCallReturnDefault method to wrap the RestTemplate GET call to the jsonplaceholder api.

Improvements

Both interface and implementation can be improved by considering the following ideas:

  • What if you want to wait before issuing another call? maybe consider another function that offer this
  • What if the user want's to get the exception that was thrown, for example they want to log the error for their own purpose
  • What if the call doesn't actually return any value

Summary

In this blog I showed you how to implement a generic retry function in Java that can take a Supplier<T> and attempts the call based on user's preference.