Spring Boot Async Tasks with Virtual Thread

April 26, 20253 min read

Spring Boot Async Tasks with Virtual Thread

Spring Boot 3.2+ lets you combine the simplicity of @Async with JDK 21 virtual threads for ultra-lightweight concurrency. Offload work to isolated virtual threads without complex pool configs.


🌟 Why Use Virtual Thread in Spring Boot?

  • Ultra-Lightweight: Virtual threads are thousands of times cheaper than platform threads.
  • Non-Blocking: @Async methods run off the main thread, improving responsiveness.
  • Scalable: Handle high concurrency with minimal resource overhead.
  • Simple Config: Enable with a single property, no custom executors needed.

🌟 Prerequisites

  • Java Development Kit (JDK) 21 or higher
  • 📦 Spring Boot 3.2+
  • 🔤 IDE (IntelliJ IDEA, Eclipse)

🛠️ Step 1: Add Dependencies

To enable async processing, include spring-boot-starter-web in your pom.xml or build.gradle file.

Maven:

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

Gradle:

implementation 'org.springframework.boot:spring-boot-starter-web'

🛠️ Step 2: Enable Virtual Threads

Add to application.yml or application.properties:

spring: threads: virtual: enabled: true
spring.threads.virtual.enabled=true

This setting auto-configures the following:

  • applicationTaskExecutor for @Async support
  • Task scheduler for @Scheduled methods
  • Servlet container thread pools (Tomcat/Jetty) to use virtual threads

📋 Step 3: Enable Async Support

Annotate your main application class in Java or Kotlin:

package com.example.async; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication @EnableAsync public class AsyncVirtualApplication { public static void main(String[] args) { SpringApplication.run(AsyncVirtualApplication.class, args); } }
package com.example.async import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.scheduling.annotation.EnableAsync @SpringBootApplication @EnableAsync class AsyncVirtualApplication fun main(args: Array<String>) { runApplication<AsyncVirtualApplication>(*args) }

📖 Step 4: Define an Async Service

Create a service with @Async. It will run each call on a new virtual thread.

package com.example.async; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.time.LocalTime; import java.util.concurrent.CompletableFuture; @Slf4j @Service public class AsyncVirtualService { @Async public void runTask() { log.info("[{}] Async start on {}", LocalTime.now(), Thread.currentThread()); try { Thread.sleep(1000); } catch (InterruptedException ignored) {} log.info("[{}] Async end on {}", LocalTime.now(), Thread.currentThread()); } @Async public CompletableFuture<String> runAndReturn() throws InterruptedException { Thread.sleep(500); return CompletableFuture.completedFuture("Completed"); } }
package com.example.async import org.slf4j.LoggerFactory import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service import java.time.LocalTime import java.util.concurrent.CompletableFuture @Service class AsyncVirtualService { private val log = LoggerFactory.getLogger(AsyncVirtualService::class.java) @Async fun runTask() { log.info("[{}] Async start on {}", LocalTime.now(), Thread.currentThread()) try { Thread.sleep(1000) } catch (_: InterruptedException) {} log.info("[{}] Async end on {}", LocalTime.now(), Thread.currentThread()) } @Async fun runAndReturn(): CompletableFuture<String> { Thread.sleep(500) return CompletableFuture.completedFuture("Completed") } }

🔄 Step 5: Trigger via REST Controller

Expose endpoints to invoke your async methods:

package com.example.async; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/async") @RequiredArgsConstructor public class AsyncVirtualController { private final AsyncVirtualService service; @GetMapping("/run") public String triggerRun() { service.runTask(); return "Async virtual thread task triggered"; } @GetMapping("/run-return") public String triggerRunAndReturn() throws Exception { var future = service.runAndReturn(); return future.get(); } }
package com.example.async import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import lombok.RequiredArgsConstructor @RestController @RequestMapping("/async") @RequiredArgsConstructor class AsyncVirtualController(private val service: AsyncVirtualService) { @GetMapping("/run") fun triggerRun(): String { service.runTask() return "Async virtual thread task triggered" } @GetMapping("/run-return") @Throws(Exception::class) fun triggerRunAndReturn(): String { val future = service.runAndReturn() return future.get() } }

▶️ Run the App

./mvnw spring-boot:run # or gradle bootRun

🧪 Test Endpoints

Trigger void task

curl http://localhost:8080/async/run

Check logs for virtual thread start/end.

Trigger task with return

curl http://localhost:8080/async/run-return # returns "Completed"

With Spring Boot’s @Async on virtual threads, you get powerful async capabilities with minimal configuration and maximum scalability.