With virtual threads available, is reactive still worth t… — Cracked Java
// Spring Framework & Spring Boot · Reactive — WebFlux, Reactor
SeniorTheoryBig Tech

With virtual threads available, is reactive still worth the complexity?

Virtual threads (Project Loom, stable in Java 21) make blocking cheap, which removes the primary reason most teams adopted reactive — scaling I/O-bound workloads. For those apps, virtual threads give you most of the throughput with imperative code. But reactive still wins where it was always strongest: streaming, backpressure, and declarative composition. The mature answer is "it depends, and the default has shifted toward virtual threads."

What virtual threads change

The original case for WebFlux was: thread-per-request doesn't scale because OS threads are expensive and blocking parks them. Virtual threads demolish that premise. A virtual thread is cheap (kilobytes, millions of them), and when it blocks on I/O the JVM unmounts it from its carrier thread — so the carrier stays busy and you get event-loop-like efficiency while writing ordinary blocking code.

// Spring Boot: one property, JDBC/blocking code now scales
spring.threads.virtual.enabled=true

@GetMapping("/u/{id}")
User get(@PathVariable Long id) {
    return repo.findById(id);   // blocking JDBC, but on a virtual thread
}

You keep readable stack traces, debuggers that work, JDBC/JPA, ThreadLocal-based security, and the entire imperative ecosystem — and still serve high concurrency.

What virtual threads do not give you

Loom is about cheap blocking, not about data-flow semantics. Reactive remains the better tool when you need:

  • Backpressure — a consumer-driven request(n) flow-control protocol. Virtual threads have no equivalent; a fast producer still needs manual queue management.
  • Streaming — server-sent events, infinite streams, processing rows as they arrive with bounded memory. Flux models this natively.
  • Declarative composition — fan-out/fan-in across many calls with zip, flatMap, timeouts, retries, and cancellation (switchMap) expressed as a pipeline. Doing this imperatively across virtual threads is possible but clumsier.

The pin caveat

Virtual threads aren't free of footguns: blocking inside a synchronized block (or certain native calls) can pin the virtual thread to its carrier, defeating the benefit. Pre-21u fixes and library updates address most of this, but it's worth knowing.

Mark your status