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.