Leveraging Virtual Threads in an existing reactive application

Authors: Abrar Ul Haq Shaik, Jatin Yadav, Mihir Bharali, Om Sharma

Introduction

In Java software development, reactive programming has been widely adopted to build robust and scalable applications by focusing on developing asynchronous and non-blocking applications. Libraries like Project Reactor and RxJava helped developers to write code that can handle large number of I/O-bound tasks efficiently. However, reactive programming came with its own set of challenges, such as steep learning curve, complexity in debugging and error handling etc. With the introduction of Virtual Threads in Project Loom, a new paradigm shift is emerging. Virtual Threads, also known as Fibers, offer a more lightweight and efficient way to handle concurrency, allowing developers to write synchronous-looking code that’s asynchronous under the hood. By leveraging Virtual Threads, developers can write more intuitive and maintainable code, while still reaping the benefits of asynchronous programming. Despite the advantage, the challenge comes while integrating Virtual Threads into an existing reactive application as both are two different paradigms.

In this post, we discuss how we can adopt Virtual Threads in an existing reactive application and evaluate their performance impact on throughput, latency and resource utilisation.

Major challenges to overcome

1. Integration with Reactive Frameworks: Frameworks like Spring Reactor or RxJava are not designed to work with Virtual Threads. Merging Virtual Threads into an existing reactive application can lead to performance degradations.

2. Thread pool Management: Even though Virtual Threads are lightweight and can be created in large numbers, but still require a thread pool to manage, which if not configured properly can disrupt the reactive flow.

Approach

Keeping the two paradigms isolated

Paradigm Isolation

To ensure that the two paradigms don’t disrupt each other, we kept the flows isolated by ensuring existing APIs built using reactive programming have no integration with Virtual Threads, similarly, new APIs developed that use imperative style of programming with Virtual Threads do not integrate with existing reactive code.

Reactive Controller Layer

The existing reactive application utilises Project reactor framework, and the APIs run asynchronously using few reactor threads. The key components in use are as follows:
· spring-boot
· spring-webflux
· reactor-core

Reactive API is exposed via handlers of Mono<String> return type

/**
* REST controller for handling reactive web requests using WebClient.
* This controller demonstrates the use of Spring WebFlux and Reactor to perform
* non-blocking HTTP requests to an external API.
*/
@RestController
public class ReactorWebClientController {
private static final Logger logger = LoggerFactory.getLogger(ReactorWebClientController.class);
private final WebClient webClient;

/**
* Default constructor that initializes the WebClient with a base URL.
*/
public ReactorWebClientController() {
this.webClient = WebClient.builder().baseUrl("https://jsonplaceholder.typicode.com").build();
}

/**
* Handles GET requests to the "/reactor" endpoint.
* This method performs a non-blocking HTTP GET request to an external API
* and returns the response as a {@link Mono} of type {@link String}.
* @return a {@link Mono} containing the response from the external API.
*/
@GetMapping("/reactor")
public Mono<String> getExternalData() {
logger.info("Handling reactive request in thread: {}", Thread.currentThread().getName());
return webClient.get()
.uri("/posts/1")
.retrieve()
.bodyToMono(String.class)
.doOnNext(response -> logger.info("Reactive response received in thread: {}", Thread.currentThread().getName()));
}
}

Blocking Controller Layer

Before going to the Blocking Controller implementation, we will go through the configuration required to enable Virtual Threads on a Reactive Application.

Configure AsyncTaskExecutor for using Java 21 — Project Loom (Virtual Threads)

Virtual Thread, like a platform thread, is also an instance of java.lang.Thread. However, Virtual Threads aren’t tied to specific OS threads. Although code written in virtual thread still needs to run on OS thread. However, when code running in a virtual thread calls a blocking I/O operation, the Java runtime suspends the virtual thread until it can be resumed. The OS thread associated with the suspended virtual thread is now free to perform operations for other virtual threads.

Hence, we should let the blocking I/O calls (eg: network/service calls) to be executed on Virtual Threads. With this in mind, we can also use an Executor of Virtual Threads to now handle the requests based on Thread-per-request model.

/**
* Configuration class for setting up asynchronous task execution using virtual threads.
* This class enables asynchronous processing in the Spring application and provides
* a custom {@link AsyncTaskExecutor} bean that leverages virtual threads for lightweight
* and efficient task execution.
*/
@EnableAsync
@Configuration
public class ThreadConfig {

/**
* Defines a custom {@link AsyncTaskExecutor} bean that uses virtual threads.
* The executor is configured with a virtual thread factory and a custom thread name prefix.
* @return an instance of {@link AsyncTaskExecutor} configured to use virtual threads.
*/
@Bean
@Qualifier("virtualThreadAsyncTaskExecutor")
public AsyncTaskExecutor virtualThreadAsyncTaskExecutor() {
SimpleAsyncTaskExecutor simpleAsyncTaskExecutor = new SimpleAsyncTaskExecutor();
simpleAsyncTaskExecutor.setVirtualThreads(true);
simpleAsyncTaskExecutor.setThreadFactory(Thread.ofVirtual().factory());
simpleAsyncTaskExecutor.setThreadNamePrefix("virtualThread-");
return simpleAsyncTaskExecutor;
}
}

This can be achieved by configuring an AsyncTaskExecutor that spawns new Virtual Threads. Given the light-weight nature of Virtual Threads, we need not pool virtual threads for their use and can also create them on-the-go.

Configuration to serve requests using Virtual Threads

One of the key features of Spring WebFlux is its built-in configuration that enables the offloading of blocking execution requests to separate threads, distinct from the reactor threads. This design allows for more efficient resource utilisation, as it prevents blocking operations from occupying the valuable reactor threads, which are responsible for handling non-blocking I/O operations.

/**
* Configuration class for customizing the WebFlux framework.
*
* This class integrates a custom {@link AsyncTaskExecutor} to handle blocking tasks
* in a reactive WebFlux application. It ensures that blocking operations are executed
* using virtual threads for better performance and scalability.
*/
@Configuration
public class WebFluxConfig implements WebFluxConfigurer {

/**
* The {@link AsyncTaskExecutor} bean configured to use virtual threads.
* This executor is injected and used for handling blocking tasks in WebFlux.
*/
@Qualifier("virtualThreadAsyncTaskExecutor")
@Autowired private AsyncTaskExecutor virtualThreadAsyncTaskExecutor;

/**
* Configures the {@link BlockingExecutionConfigurer} to use the custom
* virtual thread-based {@link AsyncTaskExecutor}.
*
* @param configurer the {@link BlockingExecutionConfigurer} to be customized.
*/
@Override
public void configureBlockingExecution(BlockingExecutionConfigurer configurer) {
configurer.setExecutor(virtualThreadAsyncTaskExecutor);
}
}

Blocking Controller Implementation

As we introduce a new API based on the imperative programming style, we have the flexibility to define controllers using the traditional imperative approach. This is particularly suitable for our use case, where the API is intended to operate synchronously, and thus, we won’t be returning Mono<String>. Instead, we can implement the controller in a straightforward manner, as demonstrated below.

It’s important to note that while the way we define routes may not directly impact performance, the execution of these routes plays a crucial role. The nuances of route execution and its implications on performance are explored in detail later in this post.

/**
* REST controller for handling requests using virtual threads.
* This controller demonstrates the use of virtual threads for processing HTTP requests
* in a Spring Boot application. It uses a custom {@link AsyncTaskExecutor} configured
* with virtual threads to handle asynchronous tasks.
*/
@RestController
public class VirtualThreadController {
private static final Logger logger = LoggerFactory.getLogger(VirtualThreadController.class);
private final HttpClient httpClient;
private final AsyncTaskExecutor asyncTaskExecutor;

/**
* Constructor for initializing the controller with a virtual thread-based {@link AsyncTaskExecutor}.
*
* @param simpleAsyncTaskExecutor the {@link AsyncTaskExecutor} bean configured to use virtual threads.
*/
public VirtualThreadController(
@Qualifier("virtualThreadAsyncTaskExecutor") @Autowired AsyncTaskExecutor simpleAsyncTaskExecutor) {
asyncTaskExecutor = simpleAsyncTaskExecutor;
httpClient = HttpClient.newHttpClient();
}

/**
* Handles GET requests to the "/virtual-thread" endpoint.
* This method processes the request using a virtual thread and performs an HTTP GET request
* to an external API. The response is returned as a string.
*
* @return the response body from the external API.
* @throws ExecutionException if an exception occurs during asynchronous execution.
* @throws InterruptedException if the thread is interrupted while waiting for the result.
*/
@GetMapping("/virtual-thread")
public String handleRequest() throws ExecutionException, InterruptedException {
logger.info("Received virtual-thread request in thread: {}", Thread.currentThread().getName());
return asyncTaskExecutor.submit(() -> {
logger.info("Processing virtual-thread request in thread: {}", Thread.currentThread().getName());
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("Response virtual-thread request in thread: {}", Thread.currentThread().getName());
return response.body();
}).get();
}

}

What is Blocking Execution for Spring WebFlux?

In most of reactive services/APIs written using Reactor, we wrap API response with Mono<String> or Flux<String> on the controller methods, so that Spring uses the limited reactor-http-nio threads and serve the requests asynchronously.

However, if we were to return any Response type other than the Reactive types (Flux, Mono, etc.), the execution is blocking, and although they run on reactor-http-nio threads, there’s no guarantee whether the reactor threads won’t be blocked during the execution as the APIs are written in a synchronous way.

Hence, it’s recommended to offload our Blocking Execution controller to a different thread executor so that the reactive threads are solely used by Reactive APIs and let the executor handle the requests on a separate thread pool. (Refer above code)

Threading Model Prior to Java 21

For Synchronous APIs written based on Thread-per-request model, we utilise a pool of Platform threads configured with any ExecutorService and serve the request on that thread. If the execution has any CPU, I/O bound tasks, then the thread would be blocked until the tasks completes and resume the execution. Given that spawning platform threads is considered expensive and resource limited, on an enterprise level, such applications consume lot of resources to achieve the desired high throughput.

So, the option would be to switch to reactive programming and utilise the asynchronous nature of certain frameworks like Project Reactor, RxJava etc. to achieve throughput and performance.

Offloading concurrent tasks to Virtual Threads

Virtual Threads also enable us to achieve concurrency in our code execution while performing external API calls.

While the parent thread executes the code synchronously, we can submit a task to a new Virtual Thread and offload the parent thread for the next execution making it asynchronous.

We can submit multiple such tasks to new Virtual Threads, and they will run concurrently.

var future1 =
virtualThreadAsyncTaskExecutor
.submit(
() -> {
try (Response response =
httpClient1.newCall(requestPayload1).execute()) {
return objectMapper.readValue(
response.body().string(),
new TypeReference<ServiceResponse<PayloadType1>>() {});
} catch (Exception e) {
throw new RuntimeException(e);
}
});

var future2 =
virtualThreadAsyncTaskExecutor
.submit(
() -> {
try (Response response =
httpClient2.newCall(requestPayload2).execute()) {
return objectMapper.readValue(
response.body().string(),
new TypeReference<ServiceResponse<PayloadType2>>() {});
} catch (Exception e) {
throw new RuntimeException(e);
}
});

// Below execution will make sure calls happen concurrently.
var clientResponse1 = future1.get();
var clientResponse2 = future2.get();

In above code, both the tasks submitted will be executed in parallel to the main thread, and since the blocking operation is submitted on Virtual Threads, as mentioned earlier JVM should unblock the OS threads, till this Virtual Thread is blocked for response.

Performance Run Summary

  • The response time of all reactive APIs remained in line with that of previous runs -without the integration of Virtual Threads.
  • The CPU utilisation of the pods remained consistent as in previous runs.
  • The JVM thread count is increased from 450 to 700 threads
API Latency comparison

Legend: * New API using Virtual Threads.

Resource Utilisation Metrics

Resource Utilisation remained largely consistent

Further Reading

1. https://docs.spring.io/spring-framework/reference/web/webflux/config.html#webflux-config-blocking-execution

2.https://github.com/spring-projects/spring-framework/issues/30678

3. https://github.com/spring-projects/spring-framework/issues/21184

4. https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html

5. https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#virtual-threads

6. https://spring.io/blog/2022/10/11/embracing-virtual-threads

7. https://blogs.oracle.com/javamagazine/post/java-loom-virtual-threads-platform-threads

8. https://liakh-aliaksandr.medium.com/concurrent-programming-in-java-with-virtual-threads-8f66bccc6460

9. https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/task/SimpleAsyncTaskExecutor.html

Leveraging Virtual Threads in an existing reactive application was originally published in Walmart Global Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.

Introduction to Malware Binary Triage (IMBT) Course

Looking to level up your skills? Get 10% off using coupon code: MWNEWS10 for any flavor.

Enroll Now and Save 10%: Coupon Code MWNEWS10

Note: Affiliate link – your enrollment helps support this platform at no extra cost to you.

Article Link: Leveraging Virtual Threads in an existing reactive application | by Jatin Yadav | Walmart Global Tech Blog | Jul, 2025 | Medium