July 7, 2026

Spring Boot 3 + Java 21: Virtual Threads and REST API Tutorial (2026)

Updated July 7, 2026: Covers Spring Boot 3.3, Java 21 LTS, virtual thread configuration, a sample REST API, and load-testing tips. Part of the Spring Boot 2026 tutorial series.

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.

Diagram comparing Java platform threads one-to-one with OS threads versus many virtual threads sharing carrier threads
Platform threads vs Java 21 virtual threads — memory and scalability comparison

When to Use Virtual Threads in Spring Boot

Use virtual threadsStick with platform threads
REST APIs with blocking JDBC/JPACPU-heavy computation (image processing, ML inference)
Microservices calling external HTTP APIsCode using synchronized blocks on hot paths (pinning)
High concurrency, moderate latency toleranceLegacy 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: hey or 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>
Spring Initializr showing Java 21 and Spring Boot 3.3 selected for new REST API project
Spring Initializr — Java 21 + Spring Boot 3.3

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.

application.properties file with spring.threads.virtual.enabled=true highlighted in IDE
Enable virtual threads with a single Spring Boot property

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
Terminal curl command returning JSON product response from Spring Boot REST API
REST API response — each request runs on a virtual thread

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
hey load test results comparing requests per second with virtual threads enabled versus disabled
Virtual threads typically sustain higher concurrency under I/O-bound load

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

Master modern Java? Join Alkademy for live Java 21 and Spring Boot courses with real-world projects.



kindsonthegenius

Kindson Munonye is a software engineer and technical author covering machine learning, statistics, REST APIs, Python, and software engineering. He publishes free tutorials on The Genius Blog and live classes on Alkademy.GitHub · LinkedIn · About · Alkademy

View all posts by kindsonthegenius →
0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted