Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

Sometimes, we require code execution to be asynchronous for better application performance and responsiveness. Also, we may want to automatically re-invoke the code on any exception, as we expect to encounter occasional failures like a network glitch.

In this tutorial, we’ll learn to implement an asynchronous execution with automatic retry in a Spring application.

We’ll explore Spring’s support for async and retry operations.

2. Example Application in Spring Boot

Let’s imagine we need to build a simple microservice that calls a downstream service to process some data.

2.1. Maven Dependencies

First, we’ll need to include the spring-boot-starter-web maven dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.2. Implementing a Spring Service

Now, we’ll implement the EventService class’s method that calls another service:

public String processEvents(List<String> events) {
    downstreamService.publishEvents(events);
    return "Completed";
}

Then, let’s define the DownstreamService interface:

public interface DownstreamService {
    boolean publishEvents(List<String> events);
}

3. Implementing Asynchronous Execution With Retry

To implement asynchronous execution with retry, we’ll use Spring’s implementation.

We’ll need to configure the application with the async and retry support.

3.1. Adding Retry Maven Dependency

Let’s add the spring-retry into the maven dependencies:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>2.0.4</version>
</dependency>

3.2. @EnableAsync and @EnableRetry Configurations

Next we need to include the @EnableAsync and @EnableRetry annotations:

@Configuration
@ComponentScan("com.baeldung.asyncwithretry")
@EnableRetry
@EnableAsync
public class AsyncConfig {
}

3.3. Include the @Async and @Retryable Annotations

To execute a method asynchronously, we’ll need to use the @Async annotation. Similarly, we’ll annotate the method with the @Retryable annotation for retrying execution.

Let’s configure the above annotations in the above EventService method:

@Async
@Retryable(retryFor = RuntimeException.class, maxAttempts = 4, backoff = @Backoff(delay = 100))
public Future<String> processEvents(List<String> events) {
    LOGGER.info("Processing asynchronously with Thread {}", Thread.currentThread().getName());
    downstreamService.publishEvents(events);
    CompletableFuture<String> future = new CompletableFuture<>();
    future.complete("Completed");
    LOGGER.info("Completed async method with Thread {}", Thread.currentThread().getName());
    return future;
}

In the above code, we’re retrying the method in case of RuntimeException and returning the result as a Future object.

We should note that we should use Future to wrap the response from any async method. 

We should note that the @Async annotation only works on a public method and should not be self-invoked within the same class. Self invoking the method will bypass the Spring proxy call and run it in the same thread.

4. Implement Test for the @Async and @Retryable

Let’s test the EventService method and verify its asynchronous and retry behaviour with a few test cases.

First, we’ll implement a test case when there’s no error from the DownstreamService call:

@Test
void givenAsyncMethodHasNoRuntimeException_whenAsyncMethodIscalled_thenReturnSuccess_WithoutAnyRetry() throws Exception {
    LOGGER.info("Testing for async with retry execution with thread " + Thread.currentThread().getName()); 
    when(downstreamService.publishEvents(anyList())).thenReturn(true);
    Future<String> resultFuture = eventService.processEvents(List.of("test1"));
    while (!resultFuture.isDone() && !resultFuture.isCancelled()) {
        TimeUnit.MILLISECONDS.sleep(5);
    }
    assertTrue(resultFuture.isDone());
    assertEquals("Completed", resultFuture.get());
    verify(downstreamService, times(1)).publishEvents(anyList());
}

In the above test, we’re waiting for the Future completion and then asserting the result.

Then, let’s run the above test and verify the test logs:

18:59:24.064 [main] INFO com.baeldung.asyncwithretry.EventServiceIntegrationTest - Testing for async with retry execution with thread main
18:59:24.078 [SimpleAsyncTaskExecutor-1] INFO com.baeldung.asyncwithretry.EventService - Processing asynchronously with Thread SimpleAsyncTaskExecutor-1
18:59:24.080 [SimpleAsyncTaskExecutor-1] INFO com.baeldung.asyncwithretry.EventService - Completed async method with Thread SimpleAsyncTaskExecutor-1

From the above log, we confirm that the service method runs in a separate thread.

Next, we’ll implement another test case with the DownstreamService method throwing a RuntimeException:

@Test
void givenAsyncMethodHasRuntimeException_whenAsyncMethodIsCalled_thenReturnFailure_With_MultipleRetries() throws InterruptedException {
    LOGGER.info("Testing for async with retry execution with thread " + Thread.currentThread().getName()); 
    when(downstreamService.publishEvents(anyList())).thenThrow(RuntimeException.class);
    Future<String> resultFuture = eventService.processEvents(List.of("test1"));
    while (!resultFuture.isDone() && !resultFuture.isCancelled()) {
        TimeUnit.MILLISECONDS.sleep(5);
    }
    assertTrue(resultFuture.isDone());
    assertThrows(ExecutionException.class, resultFuture::get);
    verify(downstreamService, times(4)).publishEvents(anyList());
}

Finally, let’s verify the above test case with the output logs:

19:01:32.307 [main] INFO com.baeldung.asyncwithretry.EventServiceIntegrationTest - Testing for async with retry execution with thread main
19:01:32.318 [SimpleAsyncTaskExecutor-1] INFO com.baeldung.asyncwithretry.EventService - Processing asynchronously with Thread SimpleAsyncTaskExecutor-1
19:01:32.425 [SimpleAsyncTaskExecutor-1] INFO com.baeldung.asyncwithretry.EventService - Processing asynchronously with Thread SimpleAsyncTaskExecutor-1
.....

From the above log, we confirm that the service method was re-executed asynchronously four times.

5. Conclusion

In this article, we’ve learned how to implement asynchronous method with the retry mechanism in Spring.

We’ve implemented this in an example application and tried a few tests to see how it handles different use cases. We’ve seen how the asynchronous code runs on its own thread and can automatically retry.

As always, example code can be found over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.