Software Engineering & Digital Products for Global Enterprises since 2006
CMMi Level 3SOC 2ISO 27001
Menu
View all services
Staff Augmentation
Embed senior engineers in your team within weeks.
Dedicated Teams
A ring-fenced squad with PM, leads, and engineers.
Build-Operate-Transfer
We hire, run, and transfer the team to you.
Contract-to-Hire
Try the talent. Convert when you're ready.
ForceHQ
Skill testing, interviews and ranking — powered by AI.
RoboRingo
Build, deploy and monitor voice agents without code.
MailGovern
Policy, retention and compliance for enterprise email.
Vishing
Test and train staff against AI-driven voice attacks.
CyberForceHQ
Continuous, adaptive security training for every team.
IDS Load Balancer
Built for Multi Instance InDesign Server, to distribute jobs.
AutoVAPT.ai
AI agent for continuous, automated vulnerability and penetration testing.
Salesforce + InDesign Connector
Bridge Salesforce data into InDesign to design print catalogues at scale.
View all solutions
Banking, Financial Services & Insurance
Cloud, digital and legacy modernisation across financial entities.
Healthcare
Clinical platforms, patient engagement, and connected medical devices.
Pharma & Life Sciences
Trial systems, regulatory data, and field-force enablement.
Professional Services & Education
Workflow automation, learning platforms, and consulting tooling.
Media & Entertainment
AI video processing, OTT platforms, and content workflows.
Technology & SaaS
Product engineering, integrations, and scale for tech companies.
Retail & eCommerce
Shopify, print catalogues, web-to-print, and order automation.
View all industries
Blog
Engineering notes, opinions, and field reports.
Case Studies
How clients shipped — outcomes, stack, lessons.
White Papers
Deep-dives on AI, talent models, and platforms.
Portfolio
Selected work across industries.
View all resources
About Us
Who we are, our story, and what drives us.
Co-Innovation
How we partner to build new products together.
Careers
Open roles and what it's like to work here.
News
Press, announcements, and industry updates.
Leadership
The people steering MetaDesign.
Locations
Gurugram, Brisbane, Detroit and beyond.
Contact Us
Talk to sales, hiring, or partnerships.
Request TalentStart a Project
Java & JVM

Reactive Java 2025: Project Loom + Virtual Threads Best Practices

SS
Sukriti Srivastava
Technical Content Lead
October 6, 2025
15 min read
Reactive Java 2025: Project Loom + Virtual Threads Best Practices — Java & JVM | MetaDesign Solutions

Introduction: The Concurrency Revolution

Java's concurrency model has undergone its most significant transformation since java.util.concurrent in Java 5. Project Loom — finalised in Java 21 LTS — introduces virtual threads, structured concurrency, and scoped values that fundamentally change how developers build concurrent applications.

Traditional reactive frameworks (Reactor, RxJava) solved the scalability problem but introduced callback complexity, debugging nightmares, and steep learning curves. Virtual threads achieve the same throughput with imperative, blocking-style code that reads linearly and debugs naturally. This guide covers virtual thread internals, structured concurrency patterns, Spring Boot integration, performance benchmarks, and migration strategies for production Java applications.

Virtual Thread Architecture and Internals

Understand how virtual threads work under the hood:

  • Continuation-Based Model: Virtual threads are implemented as continuations — when a virtual thread blocks on I/O, the JVM unmounts it from the carrier (platform) thread and stores its stack in heap memory. When the I/O completes, the continuation resumes on any available carrier thread. This enables millions of concurrent virtual threads on a handful of platform threads.
  • Carrier Thread Pool: Virtual threads execute on a ForkJoinPool of carrier threads (default size = available processors). The scheduler multiplexes virtual threads onto carriers — when one blocks, another immediately takes its place. This eliminates the idle-thread waste of traditional thread-per-request models.
  • Stack Memory: Platform threads allocate ~1MB stack memory each (OS-managed). Virtual threads use dynamically-sized heap-allocated stacks — starting at just a few KB and growing as needed. This reduces memory footprint by 100–1000x for I/O-heavy workloads.
  • Pinning: Virtual threads can become "pinned" to carrier threads when executing inside synchronized blocks or native methods — the carrier thread can't be reused while pinned. Monitor pinning with -Djdk.tracePinnedThreads=full and migrate synchronized to ReentrantLock to eliminate pinning.
  • Thread Identity: Each virtual thread has its own thread ID, name, and interrupt status — they're full Thread instances. Existing code using Thread.currentThread(), thread interruption, and thread-based debugging works unchanged with virtual threads.

Structured Concurrency and Scoped Values

Build reliable concurrent code with structured patterns:

  • StructuredTaskScope: Group related concurrent tasks so they succeed or fail as a unit. ShutdownOnFailure cancels all subtasks when any fails — preventing resource waste. ShutdownOnSuccess returns the first successful result and cancels remaining tasks — perfect for redundant service calls.
  • Error Propagation: Structured concurrency propagates exceptions naturally — if a subtask throws, the scope catches it, cancels siblings, and re-throws in the parent. No more lost exceptions in callback chains or unhandled CompletableFuture failures that silently swallow errors.
  • Scoped Values (JEP 481): Replace ThreadLocal with ScopedValue for virtual thread-safe context propagation. ScopedValues are immutable within a scope, automatically cleaned up when the scope exits, and inherited by child virtual threads. Use for request context (user ID, trace ID, locale) that flows through the call stack.
  • Task Decomposition: Structure concurrent work as task trees — parent tasks spawn child subtasks that run concurrently within a scope. The parent waits for all children, handles their results, and ensures cleanup. This mirrors try-with-resources semantics for concurrent operations.
  • Cancellation Propagation: When a scope is cancelled (timeout, failure, or explicit cancellation), all subtasks receive interruption. Virtual threads respond to interruption on any blocking operation — I/O, sleep, lock acquisition — ensuring prompt cancellation without polling loops.

Spring Boot and Framework Integration

Leverage virtual threads in Spring Boot 3.2+ applications:

  • Auto-Configuration: Enable virtual threads with a single property: spring.threads.virtual.enabled=true. Spring Boot automatically configures Tomcat/Jetty to use virtual threads for request handling — each HTTP request runs on its own virtual thread instead of a pooled platform thread.
  • Servlet Container: Tomcat 10.1+ with virtual threads handles thousands of concurrent requests without thread pool exhaustion. Traditional Tomcat limits at ~200 threads (200MB memory); with virtual threads, the same server handles 10,000+ concurrent connections with minimal memory overhead.
  • Database Access: JDBC with virtual threads is naturally non-blocking — virtual threads unmount during database I/O, freeing carrier threads for other work. HikariCP connection pool sizing becomes the bottleneck, not thread count. Consider increasing pool size or using PgBouncer for connection multiplexing.
  • WebClient Migration: Applications using WebClient for non-blocking I/O can migrate to synchronous RestClient with virtual threads — achieving the same throughput with simpler, debuggable code. RestClient + virtual threads = WebClient throughput + imperative simplicity.
  • Transaction Management: Spring's @Transactional works seamlessly with virtual threads. Transaction context flows through ScopedValues. For concurrent database operations within a transaction, use StructuredTaskScope to coordinate multiple queries while maintaining ACID guarantees.

Performance Benchmarks and Real-World Results

Quantified virtual thread performance advantages:

  • Throughput: REST API benchmark (Spring Boot 3.2, PostgreSQL, 1000 concurrent users): Platform threads (200 pool) — 2,400 req/s, virtual threads — 8,700 req/s (3.6x improvement). The bottleneck shifts from thread count to database connection pool and actual I/O latency.
  • Memory: 10,000 concurrent connections: Platform threads consume ~10GB (1MB × 10K); virtual threads consume ~50MB (5KB × 10K). This 200x reduction enables running high-concurrency workloads on smaller instances, cutting cloud infrastructure costs by 60–80%.
  • Latency: P99 latency under load: Platform threads (200 pool, 1000 concurrent) — 850ms (queueing); virtual threads — 45ms (no queueing). Virtual threads eliminate request queueing because there's no thread pool to exhaust — every request gets its own thread immediately.
  • Startup: Application startup with virtual threads is 15–20% faster — Spring's bean initialization runs on virtual threads, parallelising I/O-heavy startup tasks (database schema validation, cache warming, health checks).
  • CPU-Bound Caveat: For CPU-bound workloads (computation, serialisation, encryption), virtual threads provide no benefit — carrier thread count limits parallelism. Use platform threads with ForkJoinPool for CPU-intensive work and virtual threads for I/O-intensive work.

Transform Your Publishing Workflow

Our experts can help you build scalable, API-driven publishing systems tailored to your business.

Book a free consultation

Migration Strategy from Reactive to Loom

Migrate existing reactive codebases incrementally:

  • Phase 1 — Assessment: Identify reactive code patterns — Mono/Flux chains, WebClient calls, reactive repositories. Categorise by complexity: simple chains (direct migration), complex orchestration (requires restructuring), and streaming (may stay reactive). Profile I/O vs CPU distribution.
  • Phase 2 — Infrastructure: Upgrade to Java 21 LTS, Spring Boot 3.2+, and compatible libraries. Enable virtual threads and verify existing code works unchanged. Monitor for pinning issues with -Djdk.tracePinnedThreads. Replace synchronized blocks with ReentrantLock where pinning occurs.
  • Phase 3 — Incremental Migration: Start with leaf services — convert reactive endpoints to imperative one at a time. Replace WebClient with RestClient, reactive repositories with blocking JPA/JDBC, and Mono/Flux chains with sequential code using StructuredTaskScope for parallel operations.
  • Phase 4 — Testing: Load test migrated endpoints against reactive originals — verify equivalent throughput, reduced latency, and lower memory usage. Compare error handling behaviour — ensure exceptions propagate correctly without reactive error operators.
  • Phase 5 — Keep Reactive Where Appropriate: Streaming use cases (SSE, WebSocket, infinite sequences) may benefit from reactive streams. Backpressure-heavy scenarios (rate-limited consumers) work naturally with reactive publishers. Hybrid architectures — virtual threads for request handling, reactive streams for event processing — are perfectly valid.

Observability and Production Monitoring

Monitor virtual thread behaviour in production:

  • Java Flight Recorder (JFR): JFR captures virtual thread events — creation, blocking, unblocking, pinning, and termination. Analyse thread lifecycle to identify long-running virtual threads, excessive blocking, and pinning hotspots. Enable with -XX:StartFlightRecording=filename=recording.jfr.
  • Micrometer Metrics: Track virtual thread metrics — active count, queued tasks, carrier thread utilisation, and task completion rate. Export to Prometheus/Grafana for dashboards showing concurrency patterns and capacity utilisation.
  • Thread Dumps: Virtual thread dumps include thousands of threads — use jcmd <pid> Thread.dump_to_file -format=json for structured output. Filter by thread state (BLOCKED, WAITING) to identify bottlenecks. Group by stack trace to find common blocking points.
  • Distributed Tracing: Micrometer Tracing and OpenTelemetry work with virtual threads — trace context propagates through ScopedValues. Spans show actual I/O wait time rather than thread pool queueing, providing accurate latency attribution for each service call.
  • Health Indicators: Monitor carrier thread pool saturation, virtual thread creation rate, pinning frequency, and GC pressure from heap-allocated stacks. Alert on carrier pool exhaustion (all carriers pinned) and excessive virtual thread creation (memory pressure).

Conclusion and MDS Java Services

Project Loom represents the most significant evolution in Java concurrency — delivering reactive-level throughput with imperative code simplicity. Key takeaways:

  • Use virtual threads for I/O-bound work — HTTP requests, database queries, file operations. Platform threads for CPU-bound computation.
  • Adopt structured concurrency — StructuredTaskScope and ScopedValues replace error-prone CompletableFuture chains with reliable, cancellable task trees.
  • Migrate incrementally — Convert reactive endpoints one at a time, validate with load testing, and keep reactive streams where backpressure and streaming are genuinely needed.
  • Monitor production carefully — Track pinning, carrier utilisation, and virtual thread lifecycle with JFR and Micrometer.

MetaDesign Solutions provides expert Java modernisation services — from Project Loom migration and virtual thread adoption through Spring Boot 3.2+ upgrades, reactive-to-imperative codebase transformation, structured concurrency implementation, performance benchmarking, and production monitoring setup for enterprise Java applications.

FAQ

Frequently Asked Questions

Common questions about this topic, answered by our engineering team.

Project Loom introduces virtual threads — lightweight threads managed by the JVM that use heap-allocated stacks (~5KB vs ~1MB for platform threads). They enable millions of concurrent threads, eliminate callback-based programming, and provide imperative blocking code that achieves reactive-level throughput with simpler debugging and maintenance.

Use Loom for greenfield Java 21+ projects, I/O-heavy workloads, simpler code maintenance, and migrating legacy blocking code. Keep Reactor/RxJava for streaming use cases (SSE, WebSocket), backpressure-heavy scenarios, existing mature reactive codebases, and when you need reactive stream operators for complex event processing.

Spring Boot 3.2+ supports virtual threads with a single property: spring.threads.virtual.enabled=true. Tomcat/Jetty automatically use virtual threads for request handling, JDBC operations unmount during I/O, and @Transactional works seamlessly. RestClient + virtual threads replaces WebClient for most use cases.

In benchmarks: 3.6x throughput improvement for I/O-heavy REST APIs, 200x memory reduction (50MB vs 10GB for 10K connections), P99 latency from 850ms to 45ms (eliminating thread pool queueing), and 15–20% faster startup. No benefit for CPU-bound workloads.

Pinning occurs when virtual threads execute inside synchronized blocks or native methods — the carrier thread can't be reused. Detect with -Djdk.tracePinnedThreads=full. Fix by replacing synchronized with ReentrantLock, avoiding long-running synchronized sections, and minimising native method calls on virtual threads.

Discussion

Join the Conversation

Ready when you are

Let's build something great together.

A 30-minute call with a principal engineer. We'll listen, sketch, and tell you whether we're the right partner — even if the answer is no.

Talk to a strategist
Need help with your project? Let's talk.
Book a call