Problems and Solutions when using Async in Spring Boot

Serdar A.
3 min readSep 17, 2024

--

Hi everyone;

When you use asynchronous in spring boot project, sometimes you will face some problems. I will some solutions about this problems in this article.

https://refactorizando.com/wp-content/uploads/2022/10/@async-con-spring-boot.jpg
  1. Using without @EnableAsync annotation problem

Spring Boot doesn’t enable asynchronous support by default. If you don’t use @EnableAsync annotation in configuration class, you can not support asynchronous execution.

Fix :

  @Configuration
@EnableAsync
public class MyServiceAsyncConfig {

...
}

2. Using default thread pool problem

When using the @Async annotation by default, in actually, your app uses SimpleAsyncTaskExecutor thread pool. In actually this is not true thread pool.When use SimpleAsyncTaskExecutor thread pool, your app can not thread reuse.Your system will be created new one, when it is called. After a while, your system continuously created thread and due to increasing memory using, finally cause an OutOfMmemoryError.

Fix :

It is recommended to use custom thread pool configuration. Called ThreadPoolTaskExucutor.

  @Configuration
@EnableAsync
public class MyServiceAsyncConfig implements AsyncConfigurer {


@Bean(name="myTaskExecutor")
public TaskExecutor myTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setThreadNamePrefix("Async-");
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setMaxPoolSize(30);
threadPoolTaskExecutor.setKeepAliveSeconds(60);
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
threadPoolTaskExecutor.setQueueCapacity(100);
threadPoolTaskExecutor.setRejectedExecutionHandler(new MyTaskExecutionHandlerImpl());
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;

}
}

...

public class MyTaskExecutionHandlerImpl implements RejectedExecutionHandler {

@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

try {
executor.getQueue().put(r);
}
catch (InterruptedException e) {

log.error(e);
throw new RejectedExecutionException(e.getMessage(), e);
}

}
}

Note : you can change configuration of corePoolSize, maxPoolSize,queueCapacity parameter, according to your system requirements.

3. Wrong configuration of corePoolSize , maxPoolSize and queueCapacity

If you don’t understand this configurations, you face a lot of problems in your app.

  • If the number of threads is less than the corePoolSize, create a new Thread to run a new task.
  • If the number of threads is equal (or greater than) the corePoolSize, put the task into the queue.
  • If the queue is full, and the number of threads is less than the maxPoolSize, create a new thread to run tasks in.
  • If the queue is full, and the number of threads is greater than or equal to maxPoolSize, reject the task.

You can read details : Rules of a ThreadPoolExecutor pool size

4. Using ThreadPoolTaskExecutor with SecurityContextHolder problem

When we use method which has @Async annotation and this asynchronous method consume Rest APIs using RestTemplate or FeignClient. If you debug your code, you see SecurityContextHolder is null.Because spring security auth is bound to ThreadLocalPool.

If you want to use SecurityContextHolder passed with @Async everytime, you change your configuration and use DelegatingSecurityContextAsyncTaskExecutor.

Fix:

  @Configuration
@EnableAsync
public class MyServiceAsyncConfig implements AsyncConfigurer {


@Bean(name="myTaskExecutor")
public TaskExecutor myTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setThreadNamePrefix("Async-");
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setMaxPoolSize(30);
threadPoolTaskExecutor.setKeepAliveSeconds(60);
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
threadPoolTaskExecutor.setQueueCapacity(100);
threadPoolTaskExecutor.setRejectedExecutionHandler(new MyTaskExecutionHandlerImpl());
threadPoolTaskExecutor.initialize();
return new DelegatingSecurityContextAsyncTaskExecutor(threadPoolTaskExecutor);

}
}

5. Using asynchronous method and call it in same class

For example :

@Service
public class MyTaskService {

@Autowired
private MyOtherService;

public void callExample()
{
asyncMethod(); // call
}

@Async
public void asyncMethod()
{
myOtherService.asyncProcess();
}
}

If an asynchronous method is called from other method they are in same service. It will not work asynchronous. Because @Async must be trigered from proxy mecahniasm.

Fix :

    @Service
public class MyTaskService {

@Autowired
private MyOtherService;

public void callExample()
{
myOtherService.asyncProcess(); //call async
}
}


@Service
public class MyOtherService {


@Async
public void asyncProcess()
{
//...
}
}

6. Asynchronous ordering problem

Asynchronous method’s execution is non-blocking. So every asynchronous task are any ordering. But If you need, when a task (Task1) is completed and other task (Task2) consume/process result of Task1. You can use thenCompose method of CompletableFuture

  @Service
public class MyTaskService {

@Autowired
private MyOtherService;

public void callExample()
{
CompletableFuture<String> futureA = myOtherService
.asyncProcessA()
.thenCompose(
result -> myOtherService.asyncProcessB());


String lastResult = futureA.get();
}
}


@Service
public class MyOtherService {


@Async("myTaskExecutor")
public CompletableFuture<String> asyncProcessA()
{
return CompletableFuture.completedFuture("A Task");
}

@Async("myTaskExecutor")
public void asyncProcessB()
{
return CompletableFuture.completedFuture("B Task");
}
}

Thanx for reading. I hope, it was useful for you.

--

--

Serdar A.
Serdar A.

Written by Serdar A.

Senior Software Developer & Architect at Havelsan Github: https://github.com/serdaralkancode #Java & #Spring & #BigData & #React & #Microservice