Executing background tasks using @Async annotation in Spring Boot

When developing an application we will encounter scenarios where some operations require more time to finish.

For example, consider the scenario where an application provides an option for its users to download all their data. When a download request is received, the application will start to collect all the data of the user. Then make a compressed file of this data and send it to the user. This will take a minimum of 5 minutes to complete.

If this operation is executing in the main thread of the request, then we will be blocked until the task is finished. So, How can we solve this? This long-running task needs to be moved to another thread.

The Spring boot framework provides a feature to execute tasks in a background thread using the @Async annotation. Let's see how to execute background tasks in Spring Boot using the @Async annotation.

I've created a sample project named asyncDemo using the spring initializer with the following structure

GitHub Link: spring-boot-async-demo

The REST Endpoints

  1. /execute:- This will simulate a time-consuming task by introducing a 3-minute delay and writing a random number to a test database. Returns a taskId

  2. /fetch/{taskId}:- retrieves the generated random number

Steps to make a method asynchronous

  1. Create a thread pool executor configuration

  2. Enable asynchronous method execution capability using @EnableAsync annotation

  3. Annotate the required method with @Async

Add the following items to the project

  1. Rest controller class

  2. Entity class

  3. Service class

  4. Repository class

AsyncController.java (Rest API Controller)

package com.gintophilip.asyncDemo.controller;

import com.gintophilip.asyncDemo.entities.RandomUUID;
import com.gintophilip.asyncDemo.service.RandomUUIDService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Optional;
import java.util.Random;

@RestController
@RequestMapping("/api/")
public class AsyncController {
    @Autowired
    RandomUUIDService userService;
    @PostMapping("/execute")
    ResponseEntity<Long> timeConsumingTask(){
        Long taskId = new Random().nextLong(900) + 100;
        userService.timeConsumingTask(taskId);
        return ResponseEntity.ok(taskId);
    }
    @GetMapping("/fetch/{taskId}")
    ResponseEntity<Optional<RandomUUID>> fetchResult(@PathVariable Long taskId){
        return ResponseEntity.ok(userService.fetchResult(taskId));
    }
}

RandomUUIDService.java (Service)

package com.gintophilip.asyncDemo.service;

import com.gintophilip.asyncDemo.entities.RandomUUID;
import com.gintophilip.asyncDemo.repositories.RandomUUIDRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.Optional;
import java.util.UUID;

@Service
public class RandomUUIDService {
    @Autowired
    RandomUUIDRepository randomUUIDRepository;
    Logger logger = LoggerFactory.getLogger(RandomUUIDService.class);
    public void timeConsumingTask(Long taskId) {
        try {
            String id = String.valueOf(UUID.randomUUID());
            Thread.sleep(1000);
            RandomUUID uuid = new RandomUUID();
            uuid.setId(taskId);
            uuid.setUUID(id);
            randomUUIDRepository.save(uuid);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public Optional<RandomUUID> fetchResult(Long taskId) {
        return randomUUIDRepository.findById(taskId);
    }
}

RandomUUID.java (Entity)

package com.gintophilip.asyncDemo.entities;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class RandomUUID {
    @Id
    private Long id;
    private String UUID;

    public RandomUUID() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUUID() {
        return UUID;
    }

    public void setUUID(String UUID) {
        this.UUID = UUID;
    }
}

RandomUUIDRepository.java (Repository)

package com.gintophilip.asyncDemo.repositories;

import com.gintophilip.asyncDemo.entities.RandomUUID;
import org.springframework.data.jpa.repository.JpaRepository;

public interface RandomUUIDRepository extends JpaRepository<RandomUUID,Long> {
}

Now run the application and call the /execute endpoint. You will see that to receive the response it took 3 minutes.

Let's solve this problem by using the @Async annotation

Adding asynchronous method execution capability

Add @EnableAsync at AsyncDemoApplication class

@SpringBootApplication
@EnableAsync
public class AsyncDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(AsyncDemoApplication.class, args);
    }

}

Create the thread pool executor configuration

ThreadPoolConfig.java

Let's give an initial thread pool size of 5 and the name for the thread as async_thread

package com.gintophilip.asyncDemo.configurations;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class ThreadPoolConfig {
    @Bean("asyncExecutor")
    public ThreadPoolTaskExecutor executor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(5);
        executor.setThreadNamePrefix("async_thread");
        executor.initialize();
        return executor;
    }
}

Configure the method to run as an asynchronous task

Annotate the method timeConsumingTask inside the RandomUUIDService.java class with @Async("asyncExecutor")

We can specify which task executor to use by including the bean name if we have multiple task executor configurations. Here we use the "asyncExecutor" bean.

    @Async("asyncExecutor")
    public void timeConsumingTask(Long taskId) {
        logger.info("Thread name :{}",Thread.currentThread().getName());
        try {
            String id = String.valueOf(UUID.randomUUID());
            //simulate task by sleeping for 3 minute
            Thread.sleep(180000);
            RandomUUID uuid = new RandomUUID();
            uuid.setId(taskId);
            uuid.setUUID(id);
            randomUUIDRepository.save(uuid);
            logger.info("task completed");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

We also added a log message to show the thread name of the running async task

The async method only supports Future and void as return type

Now run the application and call the /execute endpoint. You will notice that it returned a task ID as a response.

After returning the response the timeConsumingTask method will execute on the thread named async_thread

Check the log. You will see the thread name we logged from the timeConsumingTask method

[  async_thread1] c.g.asyncDemo.service.RandomUUIDService  : Thread name :async_thread1
[  async_thread1] c.g.asyncDemo.service.RandomUUIDService  : task completed

After the completion of the task, we can get the result by calling /fetch/{taskId}