AJaiCodes logoAJaiCodes
HomeArticlesAbout
HomeArticlesReactive Programming (RxJava) vs Virtual Threads (Java 21+)

Reactive Programming (RxJava) vs Virtual Threads (Java 21+)

Ajanthan Sivalingarajah
·Mar 02, 2026·6 min read
JavaConcurrencyReactive SystemsJVM Internals
4 views
Reactive SystemsProgrammingSoftware Architecture
Threads and Concurrency in Modern Java: A Technical Deep DiveJava Concurrency Thread Types Explained with Code (JDK 25 LTS Deep Dive)

AjaiCodes

A modern tech blog platform where developers share knowledge, insights, and experiences in software engineering and technology.

Quick Links

  • Home
  • Articles
  • About

Legal

  • Privacy Policy
  • Terms of Service

© 2026 AjaiCodes. All rights reserved.

Reactive Programming (RxJava) vs Virtual Threads (Java 21+)#

Concurrency Models, Execution Semantics, Resource Utilization, and Architectural Trade-offs#

Modern Java offers two fundamentally different approaches to handling concurrency:

  1. Reactive Programming (RxJava / Reactor-style pipelines)
  2. Virtual Threads (Project Loom, Java 21+)

At first glance, both aim to solve scalability and high-concurrency problems. However, they operate at entirely different abstraction levels and are optimized for different trade-offs.

This article provides:

  • Clear, production-style code examples
  • Detailed explanations after each example and diagram
  • Internal execution models
  • Resource usage analysis
  • Logging & debugging considerations
  • Architectural decision guidance

Target audience: Beginners → Senior Engineers → Architects


1. Conceptual Difference#

Core Idea#

AspectRxJava (Reactive)Virtual Threads
ModelAsynchronous, event-drivenSynchronous style
Thread usageFew threads, many tasksMany lightweight threads
State handlingPropagated through pipelineStack-based
BackpressureBuilt-inNot inherent
DebuggabilityHarderEasier
Cognitive loadHigherLower

2. Mental Model Comparison#

Reactive Model (Event-driven)#

Client → Publisher → Operator → Operator → Subscriber

Execution is callback-driven and asynchronous.

Virtual Thread Model (Thread-per-task)#

Client → Virtual Thread → Blocking Code → Result

Execution appears sequential.


3. RxJava Deep Dive#

3.1 Basic Example#

import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers;

public class RxJavaExample {

    public static void main(String[] args) throws InterruptedException {

        Observable.fromCallable(() -> fetchUser())
                .subscribeOn(Schedulers.io())
                .map(user -> enrichUser(user))
                .observeOn(Schedulers.computation())
                .subscribe(
                        result -> System.out.println("Result: " + result),
                        error -> error.printStackTrace()
                );

        Thread.sleep(2000); // Keep JVM alive
    }

    static String fetchUser() {
        sleep(500);
        return "User";
    }

    static String enrichUser(String user) {
        return user + " Enriched";
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }
}

Detailed Explanation#

Step-by-step execution:#

  1. fromCallable() wraps a blocking method.
  2. subscribeOn(Schedulers.io()) schedules upstream execution on IO thread pool.
  3. map() transforms emitted value.
  4. observeOn() switches downstream execution context.
  5. subscribe() triggers pipeline execution.

Important:

  • Execution is lazy.
  • Work does not begin until subscribe() is called.
  • Thread switching is explicit and composable.

RxJava Execution Model (ASCII)#

           subscribe()
                |
                v
       +------------------+
       | IO Scheduler     |
       | (Thread Pool)    |
       +------------------+
                |
             fetchUser()
                |
                v
           map()
                |
       +------------------+
       | Computation Pool |
       +------------------+
                |
           Subscriber

Mermaid Flow#

flowchart LR A[Observable] --> B[subscribeOn IO] B --> C[map] C --> D[observeOn Computation] D --> E[Subscriber]

4. Virtual Threads Deep Dive#

4.1 Basic Example#

import java.util.concurrent.Executors;

public class VirtualThreadExample {

    public static void main(String[] args) throws Exception {

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {

            for (int i = 0; i < 5; i++) {
                executor.submit(() -> {
                    String user = fetchUser();
                    String enriched = enrichUser(user);
                    System.out.println(enriched);
                });
            }
        }
    }

    static String fetchUser() throws InterruptedException {
        Thread.sleep(500);
        return "User";
    }

    static String enrichUser(String user) {
        return user + " Enriched";
    }
}

Detailed Explanation#

Key points:

  • Each task runs in a virtual thread.
  • Blocking calls (like Thread.sleep) do not block OS threads.
  • JVM parks the virtual thread.
  • Underlying carrier thread becomes free.

Internally:

Virtual Thread → Park → Carrier Thread Released

This enables 100k+ concurrent blocking tasks.


Virtual Thread Model (ASCII)#

1000 Virtual Threads
        |
        v
+------------------+
| JVM Scheduler    |
+------------------+
        |
        v
8 Carrier Threads (OS)
        |
        v
CPU Cores

Mermaid Model#

flowchart TD A[Virtual Threads] --> B[JVM Scheduler] B --> C[Carrier Threads] C --> D[CPU Cores]

5. Workflow Comparison#

Reactive Workflow#

Event → Stream → Transform → Transform → Terminal Operation

State is carried inside pipeline.

Virtual Thread Workflow#

Request → Thread → Blocking I/O → Return

State is maintained in stack.


6. State Transmission#

RxJava#

State is immutable and passed between operators:

.map(user -> user + " enriched")

Each operator receives and emits a new state.

Advantages:

  • Functional purity
  • No shared mutable state

Disadvantages:

  • Harder debugging stack traces
  • Context propagation requires hooks

Virtual Threads#

State is stack-local:

String user = fetchUser();
String enriched = enrichUser(user);

Advantages:

  • Natural debugging
  • Simple exception flow

7. Resource Utilization#

RxJava#

  • Few threads
  • Event loop model
  • High CPU efficiency
  • Requires non-blocking APIs

Bad for:

  • Blocking JDBC
  • Legacy blocking libraries

Virtual Threads#

  • Many lightweight threads
  • Blocking-friendly
  • Ideal for JDBC, REST, file I/O

Memory footprint:

  • ~few KB per virtual thread
  • Stack grows dynamically

8. Logging, Debugging & Traceability#

RxJava Challenges#

  • Stack traces fragmented
  • Async boundary loses context
  • ThreadLocal propagation complex

Typical solution:

Hooks.onOperatorDebug();

But this adds overhead.


Virtual Threads#

  • Standard stack traces
  • Exceptions bubble naturally
  • Works with existing logging frameworks

Trace example:

try {
    service.call();
} catch (Exception e) {
    log.error("Failed", e);
}

No reactive context loss.


9. Pros and Cons#

RxJava#

Pros#

  • Backpressure support
  • Efficient event streaming
  • Ideal for streaming pipelines

Cons#

  • Steep learning curve
  • Debugging difficulty
  • Requires reactive ecosystem

Virtual Threads#

Pros#

  • Simple mental model
  • Minimal code changes
  • Works with existing blocking code

Cons#

  • No built-in backpressure
  • Not optimal for high-frequency streaming transformations

10. When to Use What#

ScenarioRecommended
REST API with blocking DBVirtual Threads
Streaming data pipelineRxJava
Legacy monolith modernizationVirtual Threads
High-frequency event processingRxJava
Simple microservicesVirtual Threads

11. Relationship Between Them#

They are not competitors at the same layer.

Reactive programming is a data flow paradigm. Virtual threads are a thread scheduling mechanism.

They can coexist:

  • Use Virtual Threads for request handling
  • Use Reactive streams for event pipelines

12. Final Architectural Guidance#

If you are building:

Enterprise backend (Spring Boot 3+)#

→ Default to Virtual Threads

Event-driven architecture#

→ Consider Reactive

Need simplicity and maintainability#

→ Virtual Threads

Need streaming transformation & backpressure#

→ Reactive


Closing Thought#

Reactive programming optimized for throughput efficiency.

Virtual threads optimize for developer productivity and simplicity.

In 2026, the strategic shift is clear:

Prefer structured concurrency and virtual threads unless you truly need reactive streaming semantics.

Both models remain powerful — but they solve different layers of the concurrency problem.