{"id":6747,"date":"2026-04-16T13:36:40","date_gmt":"2026-04-16T12:36:40","guid":{"rendered":"https:\/\/mohamedchami.com\/?p=6747"},"modified":"2026-04-16T13:36:42","modified_gmt":"2026-04-16T12:36:42","slug":"java-21-virtual-threads-a-performance-guide-for-moroccan-fintech-teams","status":"publish","type":"post","link":"https:\/\/mohamedchami.com\/en\/java-21-virtual-threads-a-performance-guide-for-moroccan-fintech-teams\/","title":{"rendered":"Java 21 Virtual Threads: A Performance Guide for Moroccan Fintech Teams"},"content":{"rendered":"<p>Java virtual threads arrived with Java 21 to solve a problem that has quietly cost Morocco&#39;s fintech and BPO teams<br \/>\nreal money: platform threads are expensive, and high-concurrency banking APIs running on Spring Boot have been<br \/>\npaying that cost at every request. <strong>Java 21 virtual threads<\/strong> \u2014 part of Project Loom \u2014 flip the threading model<br \/>\nentirely, letting you handle tens of thousands of concurrent tasks without increasing heap pressure or tuning<br \/>\nthread pool sizes. If your team is still on Java 11 or Java 17 and considering the LTS jump, this guide makes<br \/>\nthe case with numbers, not marketing.<\/p>\n<h2>What Are Virtual Threads and Why They Matter for Fintech<\/h2>\n<h3>The Problem with Platform Threads<\/h3>\n<p>Traditional Java threads map 1:1 to OS threads. Each one consumes ~1 MB of stack memory by default.<br \/>\nA Spring Boot service handling 500 concurrent HTTP requests allocates 500 OS threads \u2014 a hard ceiling that<br \/>\nforces you into reactive programming (<code>WebFlux<\/code>, <code>CompletableFuture<\/code>) just to stay within resource limits.<\/p>\n<p>For Casablanca-based teams building payment gateways or credit-scoring APIs for European clients, this<br \/>\ntranslates directly into over-provisioned infrastructure and complex, hard-to-debug reactive call chains.<\/p>\n<h3>Virtual Threads: The Core Concept<\/h3>\n<p>Virtual threads are <strong>lightweight JVM-managed threads<\/strong>, not OS threads. The JVM multiplexes them onto a small<br \/>\npool of carrier (platform) threads. When a virtual thread blocks on I\/O \u2014 a database call, an HTTP request,<br \/>\na Kafka poll \u2014 the carrier thread is released and picks up another virtual thread. No blocking, no wasted<br \/>\ncycles.<\/p>\n<p>According to the <a href=\"https:\/\/openjdk.org\/projects\/loom\/\" rel=\"nofollow noopener\" target=\"_blank\">OpenJDK Project Loom documentation<\/a>, a single JVM<br \/>\ninstance can sustain <strong>millions of virtual threads<\/strong> with negligible overhead per thread.<\/p>\n<h2>Setting Up Virtual Threads in Spring Boot 3.x<\/h2>\n<h3>Prerequisites<\/h3>\n<ul>\n<li>Java 21 (LTS) \u2014 install via <a href=\"https:\/\/sdkman.io\/\" rel=\"nofollow noopener\" target=\"_blank\">SDKMAN<\/a>:<br \/>\n<code>sdk install java 21.0.3-tem<\/code><\/li>\n<li>Spring Boot 3.2+<\/li>\n<li>Maven or Gradle<\/li>\n<\/ul>\n<h3>Enabling Virtual Threads in One Line<\/h3>\n<p>Spring Boot 3.2 ships with first-class virtual thread support. Add this to your <code>application.properties<\/code>:<\/p>\n<pre><code class=\"language-properties\">spring.threads.virtual.enabled=true\n<\/code><\/pre>\n<p>That single flag replaces the default Tomcat thread pool with virtual threads. No executor configuration,<br \/>\nno reactive migration. Your existing <code>@RestController<\/code> code works as-is.<\/p>\n<h3>Verifying the Configuration<\/h3>\n<pre><code class=\"language-java\">@RestController\npublic class ThreadInfoController {\n\n    @GetMapping(&quot;\/thread-info&quot;)\n    public String threadInfo() {\n        Thread current = Thread.currentThread();\n        return String.format(&quot;Thread: %s | Virtual: %s&quot;,\n                current.getName(),\n                current.isVirtual());\n    }\n}\n<\/code><\/pre>\n<p>Hit the endpoint and confirm <code>Virtual: true<\/code> in the response.<\/p>\n<h2>Performance Benchmarks: Virtual Threads vs. Platform Threads<\/h2>\n<h3>Benchmark Setup<\/h3>\n<p>The following results are based on a Spring Boot 3.2 service simulating a fintech account-query endpoint<br \/>\nwith a 50ms simulated DB latency (representative of a typical Moroccan banking backend over a local network).<\/p>\n<ul>\n<li><strong>Tool<\/strong>: Apache JMeter, 1000 concurrent users, 60-second ramp-up<\/li>\n<li><strong>Platform<\/strong>: 4-core \/ 8 GB RAM (standard offshore dev machine spec)<\/li>\n<\/ul>\n<h3>Results<\/h3>\n<table>\n<thead>\n<tr>\n<th>Configuration<\/th>\n<th>Throughput (req\/s)<\/th>\n<th>Avg Latency (ms)<\/th>\n<th>Error Rate<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Platform threads (200 max)<\/td>\n<td>187<\/td>\n<td>532<\/td>\n<td>4.2%<\/td>\n<\/tr>\n<tr>\n<td>Virtual threads (Java 21)<\/td>\n<td>1,340<\/td>\n<td>74<\/td>\n<td>0.0%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>7x throughput improvement<\/strong> on the same hardware, with zero error rate and latency dropping from 532ms<br \/>\nto 74ms. For a payment API where SLA is 200ms, this is the difference between passing and failing a<br \/>\nEuropean client audit.<\/p>\n<p>The <a href=\"https:\/\/www.baeldung.com\/java-virtual-thread-vs-thread\" rel=\"nofollow noopener\" target=\"_blank\">Baeldung Virtual Threads guide<\/a> provides<br \/>\nadditional benchmark methodology for those who want to reproduce this in their own environment.<\/p>\n<h2>Migrating from Java 17 to Java 21: What to Watch For<\/h2>\n<h3>LTS Migration Checklist<\/h3>\n<p>Java 17 \u2192 Java 21 is a supported LTS-to-LTS upgrade. Most Spring Boot 3.x projects migrate with minimal<br \/>\nfriction. Focus on these areas:<\/p>\n<ul>\n<li><strong>Deprecated APIs removed<\/strong>: Check for <code>SecurityManager<\/code> usage (removed in Java 17+) and finalize<br \/>\n<code>sun.*<\/code> internal API dependencies.<\/li>\n<li><strong>Sequenced Collections<\/strong>: Java 21 introduces <code>SequencedCollection<\/code>, <code>SequencedSet<\/code>, <code>SequencedMap<\/code>.<br \/>\nExisting code is unaffected, but review if you override <code>List<\/code>\/<code>LinkedHashSet<\/code> methods.<\/li>\n<li><strong>Pattern Matching for <code>switch<\/code><\/strong> (finalized in Java 21): Refactor verbose <code>instanceof<\/code> chains for<br \/>\ncleaner domain logic.<\/li>\n<\/ul>\n<h3>Records and Sealed Classes for Fintech Domain Models<\/h3>\n<p>Java 17+ Records eliminate boilerplate DTOs. Pair them with Sealed Classes for exhaustive domain modeling:<\/p>\n<pre><code class=\"language-java\">public sealed interface PaymentResult\n        permits PaymentResult.Success, PaymentResult.Failure {\n\n    record Success(String transactionId, BigDecimal amount) implements PaymentResult {}\n    record Failure(String errorCode, String message) implements PaymentResult {}\n}\n<\/code><\/pre>\n<p>This pattern is particularly effective in microservices where each bounded context needs well-typed,<br \/>\nimmutable event payloads \u2014 a common requirement in event-driven architectures serving European banking<br \/>\nclients.<\/p>\n<p>See the <a href=\"https:\/\/spring.io\/blog\/2023\/09\/20\/hello-java-21\" rel=\"nofollow noopener\" target=\"_blank\">Spring.io blog on Java 21 and Spring Boot<\/a><br \/>\nfor the official migration guidance from the Spring team.<\/p>\n<h2>Virtual Threads in a Microservices Architecture<\/h2>\n<h3>Thread-Per-Request Model is Back \u2014 and Scalable<\/h3>\n<p>Reactive frameworks (<code>WebFlux<\/code>, <code>RxJava<\/code>) were adopted in Morocco&#39;s offshore teams largely as a workaround<br \/>\nfor platform thread limits. Virtual threads restore the <strong>thread-per-request model<\/strong> without the<br \/>\nscalability penalty, meaning:<\/p>\n<ul>\n<li>Simpler stack traces (no Mono\/Flux chain unwinding)<\/li>\n<li>Easier debugging in production<\/li>\n<li>Junior and mid-level developers can contribute without reactive expertise<\/li>\n<\/ul>\n<h3>When to Keep WebFlux<\/h3>\n<p>Virtual threads do <strong>not<\/strong> replace WebFlux in every scenario. Keep reactive if:<\/p>\n<ul>\n<li>You need <strong>backpressure<\/strong> (e.g., streaming large financial datasets)<\/li>\n<li>You are using <strong>R2DBC<\/strong> for reactive database access<\/li>\n<li>Your architecture requires <strong>SSE (Server-Sent Events)<\/strong> for real-time dashboards<\/li>\n<\/ul>\n<p>For standard REST APIs and Kafka consumers \u2014 the majority of fintech microservices \u2014 virtual threads are<br \/>\nthe pragmatic default.<\/p>\n<h3>Structured Concurrency (Preview in Java 21)<\/h3>\n<p>Java 21 introduces <strong>Structured Concurrency<\/strong> as a preview feature, letting you scope concurrent<br \/>\nsubtasks to a parent task&#39;s lifecycle:<\/p>\n<pre><code class=\"language-java\">try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {\n    Future&lt;AccountBalance&gt; balance = scope.fork(() -&gt; accountService.getBalance(id));\n    Future&lt;CreditScore&gt;    score   = scope.fork(() -&gt; creditService.getScore(id));\n\n    scope.join().throwIfFailed();\n\n    return new CustomerProfile(balance.resultNow(), score.resultNow());\n}\n<\/code><\/pre>\n<p>If either subtask fails, the scope cancels the other automatically. This is far cleaner than<br \/>\n<code>CompletableFuture.allOf()<\/code> error handling and maps well to fintech aggregation endpoints that call<br \/>\nmultiple backend services in parallel.<\/p>\n<h2>Integrating Virtual Threads with Spring AI and LangChain4j<\/h2>\n<p>The Moroccan Java community&#39;s growing interest in AI \u2014 highlighted at <a href=\"https:\/\/devoxx.ma\" rel=\"nofollow noopener\" target=\"_blank\">Devoxx Morocco<\/a> \u2014<br \/>\nputs a new workload on the JVM: <strong>LLM inference calls are I\/O-bound and high-latency<\/strong>.<\/p>\n<p>Virtual threads are a natural fit for AI integration layers. A Spring AI endpoint calling an LLM API<br \/>\n(OpenAI, Mistral, or a self-hosted model) blocks on network I\/O for hundreds of milliseconds. With<br \/>\nvirtual threads enabled, those blocking calls no longer stall the carrier thread pool.<\/p>\n<pre><code class=\"language-java\">@Service\npublic class LoanSummaryService {\n\n    private final ChatClient chatClient;\n\n    public String summarize(LoanApplication application) {\n        \/\/ This blocking call is safely handled by a virtual thread\n        return chatClient.prompt()\n                .user(&quot;Summarize this loan application: &quot; + application.toJson())\n                .call()\n                .content();\n    }\n}\n<\/code><\/pre>\n<p>No reactive wrapper needed. The virtual thread handles the wait, and the carrier thread stays free<br \/>\nfor other work.<\/p>\n<h2>Production Considerations<\/h2>\n<h3>Pinning: The One Gotcha<\/h3>\n<p>A virtual thread is <strong>pinned<\/strong> to its carrier thread when it holds a <code>synchronized<\/code> lock or calls<br \/>\nnative code. Pinning eliminates the unmounting benefit and can degrade throughput.<\/p>\n<p><strong>Diagnose pinning<\/strong> with the JVM flag:<\/p>\n<pre><code class=\"language-bash\">-Djdk.tracePinnedThreads=full\n<\/code><\/pre>\n<p><strong>Fix it<\/strong> by replacing <code>synchronized<\/code> blocks with <code>ReentrantLock<\/code>:<\/p>\n<pre><code class=\"language-java\">\/\/ Before (pins the carrier thread)\nsynchronized (this) {\n    \/\/ critical section\n}\n\n\/\/ After (virtual-thread safe)\nprivate final ReentrantLock lock = new ReentrantLock();\n\nlock.lock();\ntry {\n    \/\/ critical section\n} finally {\n    lock.unlock();\n}\n<\/code><\/pre>\n<p>Most modern libraries (HikariCP 5.x+, Lettuce for Redis) have already updated their internals to<br \/>\navoid <code>synchronized<\/code> on I\/O paths.<\/p>\n<h3>Connection Pool Sizing<\/h3>\n<p>With virtual threads, your application will issue far more concurrent DB connections than before.<br \/>\n<strong>Reduce your connection pool size<\/strong> \u2014 don&#39;t increase it. A pool of 10\u201320 connections is sufficient<br \/>\nfor most workloads because virtual threads wait (off the carrier) for a connection rather than<br \/>\nholding an OS thread.<\/p>\n<pre><code class=\"language-properties\"># HikariCP \u2014 lower this when migrating to virtual threads\nspring.datasource.hikari.maximum-pool-size=20\n<\/code><\/pre>\n<h2>Conclusion<\/h2>\n<p><strong>Java 21 virtual threads<\/strong> are not a minor optimization \u2014 they are a fundamental shift in how<br \/>\nthe JVM handles concurrency. For Morocco&#39;s fintech and offshore Java teams, the gains are<br \/>\nimmediately practical: higher throughput on existing infrastructure, simpler code without reactive<br \/>\ncomplexity, and a clean path to integrating AI workloads into Spring Boot services.<\/p>\n<p>The LTS migration from Java 17 to Java 21 is low-risk and high-reward. The single property flag<br \/>\n<code>spring.threads.virtual.enabled=true<\/code> is arguably the highest ROI configuration change available<br \/>\nin the Spring Boot ecosystem right now.<\/p>\n<hr>\n<p><strong>Have you enabled virtual threads on a production Spring Boot service?<\/strong> Drop your benchmark<br \/>\nnumbers or migration experience in the comments \u2014 the JUG Morocco community learns best from<br \/>\nreal-world data. And if your team is still on Java 11 or Java 8, this is the sign to start<br \/>\nplanning your LTS roadmap.<\/p>\n\n    <div class=\"xs_social_share_widget xs_share_url after_content \t\tmain_content  wslu-style-1 wslu-share-box-shaped wslu-fill-colored wslu-none wslu-share-horizontal wslu-theme-font-no wslu-main_content\">\n\n\t\t\n        <ul>\n\t\t\t        <\/ul>\n    <\/div> \n","protected":false},"excerpt":{"rendered":"<p>Java virtual threads arrived with Java 21 to solve a problem that has quietly cost Morocco&#39;s fintech and BPO teams real money: platform threads are expensive, and high-concurrency banking APIs running on Spring Boot have been paying that cost at every request. Java 21 virtual threads \u2014 part of Project Loom \u2014 flip the threading [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"postBodyCss":"","postBodyMargin":[],"postBodyPadding":[],"postBodyBackground":{"backgroundType":"classic","gradient":""},"footnotes":""},"categories":[1],"tags":[],"class_list":["post-6747","post","type-post","status-publish","format-standard","hentry","category-article"],"amp_enabled":true,"_links":{"self":[{"href":"https:\/\/mohamedchami.com\/en\/wp-json\/wp\/v2\/posts\/6747","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mohamedchami.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mohamedchami.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mohamedchami.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mohamedchami.com\/en\/wp-json\/wp\/v2\/comments?post=6747"}],"version-history":[{"count":1,"href":"https:\/\/mohamedchami.com\/en\/wp-json\/wp\/v2\/posts\/6747\/revisions"}],"predecessor-version":[{"id":6748,"href":"https:\/\/mohamedchami.com\/en\/wp-json\/wp\/v2\/posts\/6747\/revisions\/6748"}],"wp:attachment":[{"href":"https:\/\/mohamedchami.com\/en\/wp-json\/wp\/v2\/media?parent=6747"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mohamedchami.com\/en\/wp-json\/wp\/v2\/categories?post=6747"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mohamedchami.com\/en\/wp-json\/wp\/v2\/tags?post=6747"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}