Why Spring AI 2.0 Requires Java 21: Virtual Threads Replace Reactive Programming

The latest Spring AI 2.0 and Spring Cloud 2025.1 releases make virtual threads the default concurrency model, showing why Java 21 is essential for AI workloads, how Spring Cloud drops Reactive Kafka, adds MVC‑plus‑virtual‑thread support, and what pitfalls developers must avoid.

Java Architecture Diary
Java Architecture Diary
Java Architecture Diary
Why Spring AI 2.0 Requires Java 21: Virtual Threads Replace Reactive Programming

Spring AI 2.0 and Spring Cloud 2025.1: Virtual Threads Take Center Stage

Both Spring AI 2.0 (preview) and Spring Cloud 2025.1 were released together, and their changelogs reveal a common trend: virtual threads are becoming the mainstream choice in the Spring ecosystem.

Key points:

Spring AI 2.0 mandates Java 21.

Spring Cloud 2025.1 removes the Reactive Kafka binder and adds MVC + virtual‑thread support to Gateway.

Java 21’s headline feature is virtual threads, which promise high‑concurrency, non‑blocking performance without the complexity of reactive programming.

1. Why Spring AI 2.0 Insists on Java 21

"This major platform upgrade aligns Spring AI with the latest Spring ecosystem."

Spring AI needs to keep pace with the rest of Spring, and the driving factor is the concurrency profile of AI applications.

AI workloads’ concurrency nightmare

Complex AI agents involve many I/O operations:

Analyze user intent (LLM call, 2‑5 s)

Query multiple data sources (DB, vector store, external API, each 0.5‑2 s)

Recursive tool calls (each may invoke another LLM)

Evaluate results (LLM‑as‑a‑Judge, another LLM call)

Retry and rollback logic

A single agent can trigger dozens of I/O calls; thousands of concurrent agents would overwhelm a traditional thread pool.

WebFlux can handle the load but at the cost of massive code complexity, making AI agent orchestration hard to maintain.

Virtual threads simplify AI agents

Spring AI 2.0 leverages Java 21’s virtual threads to optimize the underlying HTTP client and connection pool. When a virtual thread blocks, it automatically suspends and releases the platform thread, allowing thousands of virtual threads to share a few platform threads.

This enables developers to write straightforward synchronous code while gaining asynchronous performance, dramatically lowering the barrier to building high‑performance AI gateways.

Conclusion: AI’s heavy I/O pattern + mature virtual‑thread support makes Java 21 the obvious choice.

2. Spring Cloud 2025.1: The Reactive “Great Retreat”

The release notes list two major changes:

Removal of the Reactive Kafka binder (expected, as Reactor Kafka is no longer maintained).

Gateway now supports MVC + virtual threads.

The first change is a natural consequence of the virtual‑thread shift. The second is a game‑changer: the previously reactive‑only Gateway now offers an MVC‑based version that fully supports virtual threads.

Configuration comparison (simplified):

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service

vs.

spring:
  cloud:
    gateway:
      mvc:
        routes:
          - id: user-service
            uri: lb://user-service
      threads:
        virtual:
          enabled: true

Both configurations look identical, but the underlying execution model differs: the virtual‑thread version runs on platform threads that are multiplexed, delivering better performance and simpler code.

Community feedback confirms that the virtual‑thread gateway outperforms the reactive version while being far easier to understand.

3. Why WebFlux Fell Short

When WebFlux was introduced (2017), platform threads were expensive, and the C10K problem drove the adoption of event‑loop‑based reactive programming. However, the learning curve is steep:

Understanding Mono, Flux, map, flatMap, switchMap, zipWith, back‑pressure, schedulers, and error handling.

Code readability suffers due to deep nesting of operators like flatMap and switchIfEmpty.

Reactive stacks require non‑blocking drivers (R2DBC, reactive Mongo, etc.), which lag behind JDBC in maturity and ecosystem support.

Performance myths have been debunked: virtual threads achieve comparable throughput with far less complexity.

In short, 99 % of scenarios are better served by virtual threads.

4. Five Common Pitfalls of Virtual Threads

Virtual threads are not a silver bullet. Below are five practical issues developers often encounter.

Pitfall 1: Synchronized Pinning

If a virtual thread blocks inside a synchronized block, it cannot be un‑pinned, causing the underlying platform thread to stay occupied.

public class PigOrderService {
    private final Object lock = new Object();
    public Order createOrder(OrderRequest request) {
        synchronized (lock) {
            // Blocking DB calls inside the lock
            User user = userRepository.findById(request.getUserId());
            Product product = productRepository.findById(request.getProductId());
            // The virtual thread is pinned here
        }
    }
}

Java 25 resolves this issue.

Pitfall 2: Connection‑Pool Sizing

The classic formula “pool size = CPU cores × 2” no longer applies. With thousands of virtual threads, a small pool quickly becomes a bottleneck.

Recommended: increase the pool to 50‑100 connections, but avoid making it arbitrarily large because the database itself is a limited resource.

spring:
  datasource:
    hikari:
      maximum-pool-size: 50
      minimum-idle: 10

Pitfall 3: ThreadLocal Memory Leaks

Platform threads reuse ThreadLocal values, but virtual threads are short‑lived and do not automatically clear ThreadLocal data, leading to memory bloat.

Solution: use Java 21’s ScopedValue instead of ThreadLocal, which clears itself when the scope ends.

Pitfall 4: Unbounded @Async Concurrency

Spring’s @Async defaults to SimpleAsyncTaskExecutor, which creates a new virtual thread per task. While cheap, this can lead to uncontrolled concurrency if not limited.

Pitfall 5: CPU‑Intensive Workloads

Virtual threads excel at I/O‑bound tasks but add scheduling overhead for pure CPU work (encryption, compression, image processing). The recommended pattern is to isolate CPU‑heavy tasks in a dedicated platform‑thread pool.

@Configuration
public class PigThreadPoolConfig {
    // I/O‑bound: virtual threads
    @Bean("ioExecutor")
    public ExecutorService ioExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
    // CPU‑bound: platform thread pool
    @Bean("cpuExecutor")
    public ExecutorService cpuExecutor() {
        return Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    }
}

Conclusion

Previously, using WebFlux was the only way to achieve high concurrency without exhausting platform threads, but it introduced significant complexity and ecosystem constraints.

Virtual threads let developers write simple blocking code while enjoying asynchronous performance, effectively reviving developer productivity in Java backend projects.

Spring’s two recent moves—requiring Java 21 for Spring AI and adding MVC + virtual‑thread support in Spring Cloud Gateway—signal the end of the reactive‑only era: "In 99 % of cases, virtual threads are enough." Are you ready to embrace virtual threads?

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendconcurrencyspringVirtualThreadsSpringAIJava21
Java Architecture Diary
Written by

Java Architecture Diary

Committed to sharing original, high‑quality technical articles; no fluff or promotional content.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.