Learn how to use Java 21 virtual threads with Spring Boot 3 to build REST APIs that handle thousands of concurrent I/O-bound requests without thread-pool exhaustion. This spring boot java 21 tutorial shows configuration, a working controller, and when virtual threads beat platform threads.
Prerequisites: Java basics and Spring Boot fundamentals. Complete Java Lesson 1 if you are new to Java.
Estimated time: 60–75 minutes. Difficulty: Intermediate.
What Are Virtual Threads?
Virtual threads (Project Loom, finalized in Java 21) are lightweight threads managed by the JVM rather than the operating system. A single platform thread can run thousands of virtual threads, making them ideal for I/O-bound workloads like HTTP clients, database queries, and file reads.
When to Use Virtual Threads in Spring Boot
| Use virtual threads | Stick with platform threads |
|---|---|
| REST APIs with blocking JDBC/JPA | CPU-heavy computation (image processing, ML inference) |
| Microservices calling external HTTP APIs | Code using synchronized blocks on hot paths (pinning) |
| High concurrency, moderate latency tolerance | Legacy libraries not yet virtual-thread friendly |
What You Need
- JDK 21 (LTS) — download from Adoptium
- Spring Boot 3.2+ (3.3 recommended)
- Maven 3.9+ or Gradle 8+
- Optional:
heyor Apache JMeter for load testing
Step 1: Create the Spring Boot 3 Project
At start.spring.io, select Java 21, Spring Boot 3.3.x, and add Spring Web. Name the project virtual-threads-demo.
<properties>
<java.version>21</java.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

Step 2: Enable Virtual Threads
Add one line to application.properties:
spring.threads.virtual.enabled=true
Spring Boot 3.2+ configures Tomcat to use virtual threads for request handling automatically. No custom Executor bean is required for standard MVC controllers.

Step 3: Build a REST API with Blocking I/O
Create a controller that simulates I/O latency — the classic use case for virtual threads:
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) throws InterruptedException {
// Simulate blocking database or HTTP call
Thread.sleep(200);
return new Product(id, "Widget " + id, 29.99);
}
@GetMapping
public List<Product> listProducts() throws InterruptedException {
Thread.sleep(100);
return List.of(
new Product(1L, "Widget 1", 19.99),
new Product(2L, "Widget 2", 29.99)
);
}
record Product(Long id, String name, double price) {}
}
Run the app:
mvn spring-boot:run
curl http://localhost:8080/api/products/1

Step 4: Structured Concurrency Example
Java 21’s structured concurrency lets you fan out parallel I/O safely:
@GetMapping("/aggregate/{id}")
public AggregatedProduct aggregate(@PathVariable Long id) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<Product> product = scope.fork(() -> fetchProduct(id));
Subtask<Reviews> reviews = scope.fork(() -> fetchReviews(id));
scope.join();
scope.throwIfFailed();
return new AggregatedProduct(product.get(), reviews.get());
}
}
This pattern works well in Spring services that call multiple downstream APIs — common in microservices like the InventoryMS Spring Boot API.
Step 5: Benchmark Platform vs Virtual Threads
Load-test with hey (install via Homebrew or GitHub releases):
# With virtual threads enabled (default after step 2)
hey -n 5000 -c 200 http://localhost:8080/api/products/1
# Disable virtual threads, restart, and compare:
# spring.threads.virtual.enabled=false
hey -n 5000 -c 200 http://localhost:8080/api/products/1

Frequently Asked Questions
Do I need to change my Spring Boot controller code for virtual threads?
No. Enable spring.threads.virtual.enabled=true and existing blocking MVC controllers run on virtual threads automatically. Avoid wrapping every call in CompletableFuture unless you have a specific reason.
Are virtual threads available in Java 17?
Virtual threads were preview/incubator in Java 19–20 and finalized in Java 21 LTS. Spring Boot 3.2+ requires Java 17 minimum but virtual thread support targets Java 21.
What is thread pinning and should I worry?
Pinning occurs when synchronized blocks or native code prevent a virtual thread from unmounting. Most Spring/JDBC drivers are pinning-safe in 2026, but profile CPU-bound synchronized code if throughput plateaus.
Do virtual threads work with Spring WebFlux?
WebFlux is already reactive/non-blocking. Virtual threads target the Servlet stack (Spring MVC + Tomcat). Use one model per service — don’t mix unnecessarily.
Can I use virtual threads with @Async?
Yes. Define a TaskExecutor bean using Executors.newVirtualThreadPerTaskExecutor() and reference it in @Async("virtualThreadExecutor").
How does this relate to the RAG chatbot tutorial?
AI chat endpoints are I/O-bound (embedding + LLM calls). Virtual threads let a RAG service handle many concurrent users — see the Spring Boot RAG tutorial.
Next Steps
- Build a RAG Chatbot with Spring Boot — AI + REST API
- Deploy Spring Boot to Kubernetes — production deployment
- Secure REST APIs with JWT in Spring Boot
- 15 Easy Free Java Tutorials
- Alkademy Java courses
Master modern Java? Join Alkademy for live Java 21 and Spring Boot courses with real-world projects.