Asynchronous processing using virtual threads and Async annotation
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Virtual threads and Spring's @Async are often discussed together, but they solve different layers of the same problem. Virtual threads are a Java runtime feature for scaling blocking work cheaply, while @Async is a Spring mechanism for offloading a method call to an executor.
Virtual Threads
What are Virtual Threads?
Virtual threads, introduced as a preview feature in Java 19, build upon the traditional thread model by providing a much lighter and more resource-efficient thread construct. Unlike platform threads that map directly to OS threads, virtual threads are managed by the Java runtime, offering high scalability with minimal overhead.
Advantages of Virtual Threads
- Efficiency: Virtual threads drastically reduce the memory footprint since they are not directly mapped to OS threads.
- Scalability: They enable creating thousands or even millions of concurrent threads, which is not feasible with traditional threads.
- Simplified Concurrency: Virtual threads minimize the complexity associated with thread pools.
Example Usage
Below is an example demonstrating how virtual threads can be utilized for asynchronous processing:
Why they are not direct alternatives
@Async answers the question, "should this method run asynchronously?" Virtual threads answer, "what kind of thread should the executor use?" That means one can sit on top of the other. A Spring method marked with @Async still needs an executor, and that executor can use either platform threads or virtual threads.
For I/O-heavy services, virtual threads are attractive because the code can stay simple and blocking while the runtime handles large amounts of waiting work efficiently.
Spring @Async in context
In a Spring application, @Async is useful when you want asynchronous execution to be declarative. A service method can return CompletableFuture, and callers do not have to manage thread submission directly.
That convenience does not remove executor choice. If the underlying executor is a poor fit for the workload, @Async alone will not create good concurrency behavior. The strongest design is often to keep @Async as the bean boundary and back it with virtual threads when the work is mostly blocking.
One important Spring caveat is proxying. A method calling another @Async method on the same instance usually does not trigger asynchronous execution because the call never passes through the Spring proxy.
That detail matters in service design. If asynchronous behavior is part of the contract, structure the call path so the invocation really crosses the proxy boundary instead of assuming the annotation alone guarantees off-thread execution.
Common Pitfalls
- Treating virtual threads and
@Asyncas if they are the same feature. - Assuming virtual threads automatically improve CPU-bound tasks.
- Using
@Asyncwithout understanding that it still depends on an executor underneath. - Forgetting that self-invocation usually bypasses Spring's proxy-based
@Asyncbehavior. - Reaching for more complicated concurrency models when simple blocking code plus virtual threads would be enough.
Summary
- Virtual threads are a Java thread model;
@Asyncis a Spring method-dispatch mechanism. - '
@Asyncdecides that work runs off the caller thread, while the executor decides how it runs.' - Virtual threads are especially useful for large numbers of blocking I/O tasks.
- Spring applications can combine
@Asyncwith virtual-thread-backed executors effectively. - Neither feature replaces the need for clear workload analysis and sensible executor design.

