Working with Async Operations using Spring Boot

Shubham Wankhede
4 min readMar 11, 2023

well it’s not new that handling large requests gives async operations edge over synchronous operations

over the last few years we have seen that the application of multithreaded and async operations are getting more and more and better as well, but working with them is not that easy, you have to understand some concepts even before you start to work on it.

spring boot on other hand made it very easy to work with async operations.

let’s understand how we can perform async operations with spring boot.

to work with async operations in spring boot application you don’t need any special dependency to be added in your pom.xml the parent starter itself contains the library spring-context which is needed to work with async operations.

to enable async in you spring boot application you have to add @EnableAsync annotation on top any configuration class

@Configuration
@EnableAsync
public class AsyncConfig {
}

next step is to write async methods for that I’m creating a service class and writing a methods which returns the CompletableFuture object and then I’m going to annotate those methods with @Async

@Service
public class UserService {

@Autowired
private UserRepo userRepo;

@Async
public CompletableFuture<List<User>> findAllUsers() throws InterruptedException {
log.info("Thread : "+Thread.currentThread().getName());
List<User> users = userRepo.findAll();
Thread.sleep(3000);
return CompletableFuture.completedFuture(users);
}

@Async
public CompletableFuture<Void> printUsers(){
return CompletableFuture.runAsync(()->{
userRepo.findAll().stream().forEach(System.out::println);
});
}
}

to test these async operation methods you can call these methods through api or from main method to check async behaviour.

let’s call the findAllUsers() method from main method check whether it will execute asynchronously or not, to test the behaviour of async operation we are adding sleep time for each execution of method.

let’s make a call to findAllUses() method and check it’s behaviour

@SpringBootApplication
public class AsyncApplication {

public static void main(String[] args) throws InterruptedException {

ApplicationContext context = SpringApplication.run(SchedulerProjectApplication.class, args);

UserService userService = context.getBean(UserService.class);

log.info(Thread.currentThread().getName()+" : Start");
userService.findAllUsers();
log.info(Thread.currentThread().getName()+" : End");

}
}
2023-03-11T14:45:15 INFO 64705 --- [main] com.SchedulerProjectApplication  : main : Start
2023-03-11T14:45:15 INFO 64705 --- [main] com.SchedulerProjectApplication : main : End
2023-03-11T14:45:15 INFO 64705 --- [task-1] com.repo.UserService : Thread : task-1

if you look at the logs you will find that start and end logs are not waiting for the complete execution of findAllUsers() method, since the findAllUsers() is async method and is taking longer time to execute, main method is not waiting for it’s response instead it is allowing other operations to perform, hence you can observe the async behaviour of @Async annotated method.

Note: you can’t expect the async behaviour of @Async method by calling it directly from a class in which you are defining it.

In simple words if define @Async method in Main class and call it from main(-) method you will observe the synchronous behaviour instead of async behaviour.

this is because when using @Async spring boot uses AOP (Aspect Oriented Programming) to create proxy of @Component class and wrap the @Async method call in Runnable and then schedule this Runnable in Executor (default Executor AsyncTaskExecutor) so if you directly call it you won’t execute through proxy and you will not the async behaviour

By default Spring boot uses AsyncTaskExecutor if you want to use Thread Pool for your task executor you can use ThreadPoolTaskExecutor where you can configure the thread pool size, max pool size and thread name prefix etc.

to configure ThreadPoolTaskExecutor use @Bean method .

@Configuration
public class AsyncConfig {
@Bean("taskExecutor")
public Executor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(3);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("User Thread");
executor.initialize();
return executor;
}
}

Note: if you are using more than one executor or any other case alway remember to keep your been name as taskExector to give priority to your bean Task Executor.

let’s try to call the findAllUsers() method multiple times from main method to check if it is using our the ThreadPoolTaskExecutor and multiple threads from configured task execturor at the same time.

@SpringBootApplication
public class AsyncApplication {

public static void main(String[] args) throws InterruptedException {

ApplicationContext context = SpringApplication.run(SchedulerProjectApplication.class, args);

UserService userService = context.getBean(UserService.class);

log.info(Thread.currentThread().getName()+" : Start");
userService.findAllUsers();
userService.findAllUsers();
log.info(Thread.currentThread().getName()+" : End");

}
}
2023-03-11T15:25:33 INFO 64705 --- [main] com.SchedulerProjectApplication  : main : Start
2023-03-11T15:25:33 INFO 64705 --- [main] com.SchedulerProjectApplication : main : End
2023-03-11T15:25:33 INFO 64705 --- [User Thread1] com.repo.UserService : User Thread1
2023-03-11T15:25:33 INFO 64705 --- [User Thread2] com.repo.UserService : User Thread2

from the above logs we can now conclude that it is using the configured ThreadPoolTaskExecutor to executor our tasks asynchronously.

Hope you enjoyed the blog.

--

--