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.
In this section, we clarify Why Use Virtual Thread in Spring Boot? and summarize the key points you will apply in implementation.
@Async methods run off the main thread, improving responsiveness.In this section, we clarify Prerequisites and summarize the key points you will apply in implementation.
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'
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@Scheduled methodsAnnotate your main application class in Java or Kotlin:
Create a service with @Async. It will run each call on a new virtual thread.
Expose endpoints to invoke your async methods:
./mvnw spring-boot:run # or gradle bootRun
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"
You now have a practical Spring Boot Async Tasks with Virtual Thread implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.
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) }
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") } }
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() } }