<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Henry Brown's Notes on the JVM]]></title><description><![CDATA[Practical notes on software development for the JVM, from Cape Town. Mostly Kotlin and Spring Boot: coroutines and structured concurrency, virtual threads, reac]]></description><link>https://blog.hbrown.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a6df6b110d8ecdf29cfd44b/8e730ee0-3168-45a3-a296-1b304136504a.png</url><title>Henry Brown&apos;s Notes on the JVM</title><link>https://blog.hbrown.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 01 Aug 2026 18:36:43 GMT</lastBuildDate><atom:link href="https://blog.hbrown.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Fan-in with Kotlin Coroutines and Flow]]></title><description><![CDATA[Sometimes you need to start several pieces of work at the same time and handle each result as soon as it is available.
The fan-in pattern is a good fit for this: many concurrent producers send their r]]></description><link>https://blog.hbrown.dev/fan-in-with-kotlin-coroutines-and-flow</link><guid isPermaLink="true">https://blog.hbrown.dev/fan-in-with-kotlin-coroutines-and-flow</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[coroutines]]></category><category><![CDATA[flow]]></category><category><![CDATA[structured concurrency]]></category><category><![CDATA[channel]]></category><dc:creator><![CDATA[Henry Brown]]></dc:creator><pubDate>Wed, 22 Jul 2026 08:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a6df6b110d8ecdf29cfd44b/8052358a-d6f9-4d50-9d29-9d83dcfedce0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sometimes you need to start several pieces of work at the same time and handle each result as soon as it is available.
The fan-in pattern is a good fit for this: many concurrent producers send their results into one stream, and one consumer collects that stream.</p>
<p>Consider comparison shopping for a pair of headphones.
If you want prices from four stores, you would not ask the first store, wait for its response, and only then ask the second store.
You would send all four requests and process the quotes in the order they arrive.</p>
<p>In Kotlin, <code>channelFlow</code> gives us a convenient way to express that pattern while keeping the result as an ordinary <code>Flow</code>.</p>
<p>The example uses the <code>kotlinx-coroutines-core</code> dependency:</p>
<pre><code class="language-kotlin">implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
</code></pre>
<p>Here is the complete example:</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.Locale
import kotlin.random.Random

data class PriceQuote(val store: String, val price: Double)

// Simulates a store API with its own latency
fun fetchPrice(store: String, product: String): Flow&lt;PriceQuote&gt; = flow {
    println("Requesting $product price from $store")
    delay(Random.nextLong(300, 2_000))
    emit(PriceQuote(store, Random.nextDouble(80.0, 120.0)))
}

fun comparePrices(product: String): Flow&lt;PriceQuote&gt; = channelFlow {
    val stores = listOf("Amazon", "Takealot", "eBay", "AliExpress")

    for (store in stores) {
        launch {
            fetchPrice(store, product).collect { quote -&gt;
                send(quote)
            }
        }
    }
}

fun main() = runBlocking {
    comparePrices("headphones").collect { quote -&gt;
        println("${quote.store}: \$${"%.2f".format(Locale.US, quote.price)}")
    }

    println("All stores answered.")
}
</code></pre>
<p>One run may produce the following output:</p>
<pre><code class="language-text">Requesting headphones price from Amazon
Requesting headphones price from Takealot
Requesting headphones price from eBay
Requesting headphones price from AliExpress
eBay: $94.13
Amazon: $101.87
AliExpress: $83.42
Takealot: $109.55
All stores answered.
</code></pre>
<p>The request is logged before each coroutine reaches <code>delay</code>, so those messages appear before their corresponding quotes.
Neither the request messages nor the quotes have a guaranteed order, although the requests will normally start in store-list order in this small example.</p>
<h2>Why use <code>channelFlow</code> instead of <code>flow</code>?</h2>
<p>A normal <code>flow { }</code> is designed for sequential emission from its own coroutine.
It preserves the coroutine context in which it is collected, so starting child coroutines inside it and calling <code>emit</code> from those children violates the flow invariant.</p>
<p><code>channelFlow</code> is designed for the case where several coroutines need to produce values for the same flow.
Its block provides a <code>ProducerScope</code>, and each child coroutine can safely call <code>send</code> on that scope.
The channel merges those values into a single flow for the collector.</p>
<p>The resulting flow is still cold.
Nothing happens when <code>comparePrices("headphones")</code> is called; the store requests start when the flow is collected.
Collecting it a second time starts four new requests and produces a new set of quotes.</p>
<h2>Why launch inside the loop?</h2>
<p>Without <code>launch</code>, the collections would run one after another:</p>
<pre><code class="language-kotlin">for (store in stores) {
    fetchPrice(store, product).collect { quote -&gt;
        send(quote)
    }
}
</code></pre>
<p><code>collect</code> is a suspending call and does not return until that store's flow completes.
Amazon would therefore finish before Takealot starts, followed by eBay and AliExpress.
The total waiting time would be close to the sum of all four delays.</p>
<p>With <code>launch</code>, each store flow is collected by a child coroutine.
All four delays can overlap, so the total waiting time is close to the slowest request rather than the sum of every request.
This is concurrency; it does not require four dedicated threads.
While one coroutine is suspended in <code>delay</code> or waiting for non-blocking I/O, another coroutine can make progress.</p>
<h2>Why does the flow wait for every store?</h2>
<p>The child coroutines launched inside <code>channelFlow</code> belong to its scope.
Because this follows structured concurrency, the flow does not complete until the builder block and all of its children have completed.</p>
<p>The loop itself finishes quickly, but its children are still fetching and sending quotes.
Only after the last child finishes does collection complete and <code>All stores answered.</code> print.
There is no need to maintain a counter, close a channel manually, or call <code>joinAll</code>.</p>
<p>Structured concurrency also defines the failure behaviour.
If one store flow throws an exception, its failure cancels the <code>channelFlow</code> and the other child coroutines, and the exception reaches the collector.
If a failed store should not stop the comparison, handle that failure inside the individual store flow and decide what result to send instead.</p>
<h2>Why use <code>send</code> instead of <code>emit</code>?</h2>
<p><code>send</code> is the channel operation provided by the <code>ProducerScope</code> inside <code>channelFlow</code>.
Each store coroutine sends its quote into the same channel, and the downstream collector receives quotes as they become available.
That interleaving of several producers into one consumer is the fan-in.</p>
<p>From <code>main</code>'s point of view, <code>comparePrices</code> is simply a <code>Flow&lt;PriceQuote&gt;</code>.
The concurrency is kept inside the producer, so callers can use normal flow operators and terminal operations without knowing how the values were produced.</p>
<h2>Cancellation and backpressure</h2>
<p>Cancellation follows the same parent-child relationship.
If the collector is cancelled or stops early, the work inside <code>channelFlow</code> and its child coroutines is cancelled as well.
For example, <code>comparePrices("headphones").first()</code> returns the first quote to arrive and cancels the remaining requests.
It finds the fastest response, not necessarily the cheapest price.</p>
<p><code>channelFlow</code> uses a buffered channel by default.
Producers can get ahead of a slow collector until that buffer is full; after that, <code>send</code> suspends until capacity becomes available.
That gives the flow backpressure without allowing producers to grow an unbounded queue.
If every send must wait for the collector, apply <code>buffer(0)</code> to the returned flow to use rendezvous behaviour.</p>
<h2>The shorter version with <code>merge</code></h2>
<p>For this example, the <code>merge</code> flow operator is the idiomatic shortcut:</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.flow.merge

fun comparePrices(product: String): Flow&lt;PriceQuote&gt; {
    val stores = listOf("Amazon", "Takealot", "eBay", "AliExpress")

    return stores
        .map { store -&gt; fetchPrice(store, product) }
        .merge()
}
</code></pre>
<p><code>merge</code> collects all of the flows concurrently and emits their values without preserving the order of the source flows.
For a straightforward fan-in, I would normally use <code>merge</code>.
When the producer needs more control over how child coroutines send values, handle callbacks, or combine different kinds of concurrent work, <code>channelFlow</code> makes the underlying pattern explicit.</p>
<p>The useful part of fan-in is that the consumer does not have to coordinate the producers.</p>
<p>Each producer works at its own pace, while the collector sees one flow of results and retains the normal completion, failure, cancellation, and backpressure behaviour of Kotlin coroutines.</p>
]]></content:encoded></item><item><title><![CDATA[Accessing Apple's on-device AI from Kotlin using apfel]]></title><description><![CDATA[If you own a modern Mac with an Apple Silicon chip and Apple Intelligence enabled, then you already have access to a local foundation model.
There is no separate model to download, no API key to creat]]></description><link>https://blog.hbrown.dev/accessing-apple-s-on-device-ai-from-kotlin-using-apfel</link><guid isPermaLink="true">https://blog.hbrown.dev/accessing-apple-s-on-device-ai-from-kotlin-using-apfel</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[apfel]]></category><category><![CDATA[openai]]></category><category><![CDATA[Apple Intelligence]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Henry Brown]]></dc:creator><pubDate>Thu, 09 Jul 2026 08:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a6df6b110d8ecdf29cfd44b/fc8f4f41-bd63-46d4-9914-f68171aeafe4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you own a modern Mac with an Apple Silicon chip and Apple Intelligence enabled, then you already have access to a local foundation model.
There is no separate model to download, no API key to create, and no token bill to worry about.</p>
<p>macOS Tahoe ships with Apple's on-device foundation model, roughly a 3B parameter LLM, and <a href="https://apfel.franzai.com/">apfel</a> gives it a very convenient front door.
It wraps Apple's <code>FoundationModels</code> framework and exposes the model in three ways:</p>
<ol>
<li>A UNIX-style command-line tool.</li>
<li>An OpenAI-compatible HTTP server.</li>
<li>An interactive chat mode.</li>
</ol>
<p>In this post I will show how to use apfel from the command line, then call the same local model from a Kotlin script using the OpenAI-compatible API.</p>
<h2>Installing apfel</h2>
<p>The installation is a single Homebrew command:</p>
<pre><code class="language-shell">brew install apfel
</code></pre>
<p>apfel requires Apple Silicon, macOS Tahoe, and Apple Intelligence to be enabled.
Once those pieces are in place, there is no extra configuration needed.</p>
<p>The important part is that the model runs on your Mac.
For short prompts, small text transformations, command explanations, shell one-liners, translations, and local scripting tasks, that is very appealing.
You get a useful local assistant without sending the prompt to a cloud model.</p>
<h2>Prompting from the terminal</h2>
<p>After installing apfel, you can prompt the model directly:</p>
<pre><code class="language-shell">apfel --stream "Can you write some Kotlin code to print Hello World to the screen?"
</code></pre>
<p>You can also use it in the normal UNIX way by redirecting output:</p>
<pre><code class="language-shell">apfel "Write a Kotlin script to print Hello World to the console without any explanation or anything else besides the script." &gt; hello.kts
</code></pre>
<p>The model is still a small local model, so you should inspect what it writes, but the output is easier to pipe into a file.</p>
<h2>Starting the OpenAI-compatible server</h2>
<p>The most interesting part for Kotlin is the server mode.
apfel can expose the local model as an OpenAI-compatible API:</p>
<pre><code class="language-shell">apfel --serve
</code></pre>
<p>By default, this starts a server on:</p>
<pre><code class="language-text">http://127.0.0.1:11434
</code></pre>
<p>If that port is already busy, or you just prefer a different port, you can specify one:</p>
<pre><code class="language-shell">apfel --serve --port 11435
</code></pre>
<p>The server exposes the familiar chat completions endpoint:</p>
<pre><code class="language-text">POST /v1/chat/completions
</code></pre>
<p>It also supports useful OpenAI-style features such as streaming, <code>GET /v1/models</code>, tool calling, <code>response_format: json_object</code>, <code>temperature</code>, <code>max_tokens</code>, <code>seed</code>, and CORS for browser clients.</p>
<p>For this post, we only need the basic chat completions endpoint.</p>
<h2>Calling apfel from Kotlin</h2>
<p>Because the server speaks the OpenAI API shape, calling it from Kotlin is straightforward.
The model name is <code>apple-foundationmodel</code>, and the API key can be any placeholder value because the local server does not require authentication by default.</p>
<p>Here is a complete Kotlin script:</p>
<pre><code class="language-kotlin">@file:DependsOn("tools.jackson.core:jackson-databind:3.0.3")

import tools.jackson.databind.json.JsonMapper
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

val baseUrl = "http://localhost:11434/v1"
val apiKey = "unused"

val body = """
    {
      "model": "apple-foundationmodel",
      "messages": [
        {
          "role": "user",
          "content": "Translate to Korean: Thank you"
        }
      ],
      "temperature": 0.2,
      "max_tokens": 100
    }
""".trimIndent()

val request = HttpRequest.newBuilder(URI.create("$baseUrl/chat/completions"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer $apiKey")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build()

runCatching {
    val response = HttpClient.newHttpClient()
        .send(request, HttpResponse.BodyHandlers.ofString())

    check(response.statusCode() in 200..299) {
        "HTTP ${response.statusCode()}: ${response.body()}"
    }

    val root = JsonMapper.shared().readTree(response.body())
    val content = root.path("choices").get(0)
        ?.path("message")?.path("content")?.asString()
        ?: error("Unexpected response shape: ${response.body()}")

    println(content)
}.onFailure { System.err.println("Error: ${it.message}") }
</code></pre>
<p>Save this as <code>apfel.main.kts</code>.
With Kotlin on your path, you can run it directly:</p>
<pre><code class="language-shell">kotlin apfel.main.kts
</code></pre>
<p>Here is the output I get:</p>
<pre><code class="language-text">감사합니다.
</code></pre>
<p>If you started apfel on the alternate port shown earlier, change the script to:</p>
<pre><code class="language-kotlin">val baseUrl = "http://localhost:11435/v1"
</code></pre>
<p>That is the main benefit of the OpenAI-compatible API.
The code is ordinary HTTP client code.
If you already have a client abstraction that talks to OpenAI, you can often point it at <code>http://localhost:11434/v1</code>, use the <code>apple-foundationmodel</code> model name, and keep most of the rest of the code the same.</p>
<h2>Checking the available model</h2>
<p>The server also exposes a models endpoint, which is useful when you want to verify that the server is running:</p>
<pre><code class="language-shell">curl http://localhost:11434/v1/models
</code></pre>
<p>For apfel, the important model id is:</p>
<pre><code class="language-text">apple-foundationmodel
</code></pre>
<p>There is no model picker here in the same sense as a cloud API or a local model manager such as Ollama.
apfel is intentionally wrapping the Apple model that is already available through macOS.</p>
<h2>What this is good for</h2>
<p>The local model is not meant to replace larger cloud-based models.
It has a smaller context window, around 4,096 tokens for input and output combined, and it is not the tool I would choose for long documents, complex code generation, or tasks that need strong factual recall.</p>
<p>Where it becomes interesting is in small, local, everyday tasks:</p>
<ol>
<li>Explaining a shell command.</li>
<li>Generating a small regex.</li>
<li>Rephrasing a short paragraph.</li>
<li>Translating a phrase.</li>
<li>Summarising a short file.</li>
<li>Turning command output into something easier to read.</li>
<li>Building a small local Kotlin utility that needs just enough language understanding.</li>
</ol>
<p>The local context is the part I like most.
Cloud models cannot see the output of <code>df -h</code>, <code>lsof</code>, <code>git log</code>, or <code>ls</code> unless you explicitly send that data to them.
With apfel, you can pipe local command output into a local model and keep the whole loop on your machine.</p>
<p>For example:</p>
<pre><code class="language-shell">df -h | apfel "Which volume is closest to full? Reply in one sentence."
</code></pre>
<p>or:</p>
<pre><code class="language-shell">git log --oneline -10 | apfel "Summarise these commits in three bullets."
</code></pre>
<p>That makes apfel feel less like a replacement for a big hosted model and more like a small language-aware UNIX tool.</p>
<h2>Final thoughts</h2>
<p>For me, the nice thing about apfel is not only that it provides CLI access to Apple's on-device model.
It is that it exposes the model through interfaces developers already know: standard input and output for shell scripts, and an OpenAI-compatible API for application code.</p>
<p>That makes it easy to experiment from Kotlin.
Start <code>apfel --serve</code>, point a small Kotlin script at <code>http://localhost:11434/v1</code>, send a normal chat completions request, and you have a private local model call running on your Mac.</p>
<p>It will not replace cloud models for every use case, and it should not be treated as if it has the same capabilities as a larger hosted model.
But for quick local automation, small text transforms, and developer workflows where privacy and zero setup matter, it is a very useful tool to have installed.</p>
]]></content:encoded></item><item><title><![CDATA[How many threads can you create with Spring Boot and Java virtual threads?]]></title><description><![CDATA[In looking at enabling virtual threads for a Spring Boot application, I began to wonder how many virtual threads I could spawn compared to platform threads.
I knew the answer would be "many more", but]]></description><link>https://blog.hbrown.dev/how-many-threads-can-you-create-with-spring-boot-and-java-virtual-threads</link><guid isPermaLink="true">https://blog.hbrown.dev/how-many-threads-can-you-create-with-spring-boot-and-java-virtual-threads</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[concurrency]]></category><category><![CDATA[virtualthreads]]></category><category><![CDATA[Java]]></category><dc:creator><![CDATA[Henry Brown]]></dc:creator><pubDate>Fri, 22 May 2026 08:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a6df6b110d8ecdf29cfd44b/e64eae15-d244-495a-8bd6-459fb4b36a5c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In looking at enabling virtual threads for a Spring Boot application, I began to wonder how many virtual threads I could spawn compared to platform threads.
I knew the answer would be "many more", but I wanted to see the difference on my own machine rather than just repeat the theory.</p>
<p>So I wrote a deliberately simple experiment.
It does not simulate a full production workload.
It does not call a database, marshal JSON, apply business rules, or exercise a web server.
It simply asks one question:</p>
<blockquote>
<p>How many parked threads can this JVM create before something gives way?</p>
</blockquote>
<p>That is still a useful question, because it shows the most important difference between platform threads and virtual threads.
Platform threads are expensive because they are backed by operating system threads.
Virtual threads are cheap because they are managed by the JVM and only use a platform thread while they are actually running.</p>
<h2>Spawning platform threads</h2>
<p>The first test creates platform threads using the normal <code>Thread</code> constructor.
Each thread immediately parks itself so that it stays alive without doing any useful work.</p>
<pre><code class="language-kotlin">import org.junit.jupiter.api.Test
import java.util.concurrent.locks.LockSupport

class PlatformThreadLimitTest {
    @Test
    fun `how many platform threads can be created`() {
        var threadCount = 0
        try {
            while (true) {
                Thread {
                    while (true) LockSupport.park()
                }.start()
                threadCount++
            }
        } catch (_: OutOfMemoryError) {
            System.err.println("Platform thread limit reached: $threadCount")
        }
    }
}

</code></pre>
<p>This is not a test I would leave in a normal build.
It is intentionally trying to exhaust a resource, and the exact failure point depends on the machine, operating system limits, JVM settings, and other processes running at the same time.</p>
<p>The important point is that every platform thread needs an operating system thread underneath it.
That means the limit is not only about Java heap.
Native memory, per-thread stack size, process limits, and the operating system scheduler all matter.</p>
<h2>Spawning virtual threads</h2>
<p>The second test creates one million virtual threads.
Again, each thread parks itself immediately.</p>
<pre><code class="language-kotlin">import org.junit.jupiter.api.Test
import java.util.concurrent.locks.LockSupport

class VirtualThreadLimitTest {
    @Test
    fun `how many virtual threads can be created`() {
        val n = 1_000_000
        repeat(n) {
            Thread.startVirtualThread {
                while (true) LockSupport.park()
            }
        }
        val rt = Runtime.getRuntime()
        val usedMb = (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024)
        println("Mounted $n virtual threads, heap used: ${usedMb}MB")
    }
}
</code></pre>
<p>This is where virtual threads feel quite different.
There is no attempt here to create a million operating system threads.
Instead, the JVM creates a million lightweight <code>Thread</code> instances whose continuations can be mounted on a much smaller number of carrier platform threads when they need to run.</p>
<p>Because the threads are parked, most of them are not mounted on carrier threads most of the time.
That is exactly the kind of situation virtual threads are designed for: lots of concurrent tasks, many of which spend most of their lives waiting.</p>
<h2>Results</h2>
<p>On my MacBook Pro M4 with 48 GB RAM, I got the following results.</p>
<h3>Platform threads</h3>
<pre><code class="language-text">Platform thread limit reached: 12253
</code></pre>
<h3>Virtual threads</h3>
<pre><code class="language-text">Mounted 1000000 virtual threads, heap used: 581MB
</code></pre>
<h2>Result analysis</h2>
<p>The platform-thread test dies in the low thousands, around 12k on my machine.
The virtual-thread test manages to create one million parked virtual threads on a normal heap size.</p>
<p>That is a remarkable difference, but it is worth being precise about what this proves.
It proves that virtual threads have a much lower per-thread footprint when they are parked or blocked in a way the JVM can unmount.
It does not prove that a Spring Boot application can handle one million useful concurrent requests.</p>
<p>Real applications have other bottlenecks:</p>
<ol>
<li>Database connection pools.</li>
<li>HTTP client connection pools.</li>
<li>Downstream service limits.</li>
<li>CPU-bound work.</li>
<li>Memory used by request objects, responses, buffers, caches, and application state.</li>
<li>Rate limits, queues, and back-pressure policies.</li>
</ol>
<p>Virtual threads remove one very old bottleneck: the need to keep a large pool of expensive platform threads around just because requests might block.
They do not remove the need to size and protect the rest of the system.</p>
<p>The number of virtual threads you can create is mainly a memory question rather than an operating-system-thread question.
If you increase the heap with <code>-Xmx</code>, you can usually create more parked virtual threads.
If each virtual thread has a deeper stack or holds more request state, you will create fewer.</p>
<h2>Why this matters for Spring MVC</h2>
<p>Spring MVC traditionally uses a request-per-thread model.
That model is wonderfully simple: a request comes in, a thread handles it, and the code can be written in a direct blocking style.</p>
<p>The downside has always been scalability.
If a request spends time waiting for a database, a REST call, a message broker, or a filesystem operation, the platform thread is still occupied while it waits.
That is one of the reasons reactive programming with Spring WebFlux became attractive for high-concurrency I/O-heavy applications.</p>
<p>Virtual threads change that trade-off.
For many I/O-bound workloads, you can keep the straightforward blocking style of Spring MVC while allowing the JVM to park blocked work cheaply.
That does not make WebFlux obsolete.
Reactive programming is still a very good fit for streaming, back-pressure, and end-to-end reactive systems.
But virtual threads make the simple blocking model scale much further than it used to.</p>
<h2>Enabling virtual threads in Spring Boot</h2>
<p>Enabling virtual threads in Spring Boot is as simple as setting a property in your <code>application.properties</code> or <code>application.yaml</code> file:</p>
<pre><code class="language-properties">spring.threads.virtual.enabled=true
</code></pre>
<p>At least Java 21 is required for this to work.
Spring Boot's reference documentation also notes that the feature affects auto-configured task execution and scheduling.
When virtual threads are enabled, Boot uses virtual-thread-friendly executor and scheduler choices in places where its auto-configuration is responsible for creating them.</p>
<p>That means this property is not only about embedded Tomcat handling requests.
It can also affect pieces such as <code>@Async</code>, Spring MVC asynchronous request handling, blocking execution support in WebFlux, and scheduled task infrastructure depending on how the application is configured.</p>
<p>There is one small Spring Boot detail that is easy to miss.
Virtual threads are daemon threads.
If your application relies on scheduled work or other virtual-thread-based infrastructure to keep the JVM alive, you may need:</p>
<pre><code class="language-properties">spring.main.keep-alive=true
</code></pre>
<p>This is called out in the Spring Boot reference documentation and is worth knowing before enabling virtual threads in a service that mostly does background or scheduled work.</p>
<h2>Things to watch out for</h2>
<p>Virtual threads are very useful, but they are not magic performance dust.
I would pay attention to at least the following when enabling them in a Spring Boot application.</p>
<h3>Pinning</h3>
<p>A virtual thread can usually unmount from its carrier thread when it blocks.
There are cases where it cannot, which is known as pinning.
Historically, blocking while inside a <code>synchronized</code> block or native method could pin the virtual thread to its carrier.
If enough virtual threads are pinned for long periods, throughput can suffer because the carrier threads are no longer free to run other virtual threads.</p>
<p>The JDK has been improving this area, but it is still worth testing your own application.
The official Java documentation and Spring Boot reference both point to tools such as JDK Flight Recorder and <code>jcmd</code> for detecting pinned virtual threads.
Spring Boot's current reference documentation also strongly recommends Java 24 or later for the best virtual-thread experience, even though Java 21 is the minimum version required.</p>
<h3>Pool sizing still matters</h3>
<p>With platform threads, the web server thread pool often acted as an accidental concurrency limit.
Turning on virtual threads can remove or greatly raise that limit.
That is usually what you want, but it means other limits become more visible.</p>
<p>If your database pool has 20 connections, then allowing 20,000 requests to reach code that all needs a database connection will not make the database faster.
It may just move the waiting from the servlet thread pool to the connection pool.</p>
<p>For that reason, virtual threads often pair well with explicit bulkheads, timeouts, rate limits, and connection-pool sizing.</p>
<h3>CPU-bound work does not become cheaper</h3>
<p>Virtual threads are excellent when work blocks or waits.
They do not give the CPU more cores.
If the workload is mostly CPU-bound, creating more virtual threads can increase scheduling overhead without improving throughput.</p>
<p>For CPU-heavy work, the usual rules still apply: measure, use bounded parallelism, and size around the available processors.</p>
<h3>Be careful with ThreadLocal usage</h3>
<p>Virtual threads are still instances of <code>java.lang.Thread</code>, and they support <code>ThreadLocal</code>.
That is useful for compatibility with existing libraries.
However, virtual threads are intended to be numerous and short-lived, so using <code>ThreadLocal</code> as a cache for expensive resources is a bad fit.</p>
<p>Use normal Spring-managed beans, connection pools, and scoped context mechanisms where appropriate.
Do not treat a virtual thread as a long-lived worker thread with reusable state attached to it.</p>
<h2>Useful references</h2>
<p>These are the pages I would keep close by when experimenting with this in a Spring Boot application:</p>
<ol>
<li><a href="https://docs.spring.io/spring-boot/reference/features/spring-application.html#features.spring-application.virtual-threads">Spring Boot reference documentation: Virtual threads</a></li>
<li><a href="https://docs.spring.io/spring-boot/reference/features/task-execution-and-scheduling.html">Spring Boot reference documentation: Task execution and scheduling</a></li>
<li><a href="https://spring.io/guides/gs/rest-service/">Spring Guide: Building a RESTful Web Service</a></li>
<li><a href="https://spring.io/guides/gs/reactive-rest-service/">Spring Guide: Building a Reactive RESTful Web Service</a></li>
<li><a href="https://spring.io/blog/2022/10/11/embracing-virtual-threads">Spring blog: Embracing Virtual Threads</a></li>
<li><a href="https://openjdk.org/jeps/444">OpenJDK JEP 444: Virtual Threads</a></li>
<li><a href="https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html">Oracle Java documentation: Virtual Threads</a></li>
</ol>
<h2>Final thoughts</h2>
<p>For me, the experiment makes the appeal of virtual threads very concrete.
With platform threads, the JVM runs into a practical wall after a few thousand parked threads.
With virtual threads, a million parked tasks is not especially dramatic.</p>
<p>That difference matters for Spring Boot applications because so many business systems are mostly waiting: waiting for databases, waiting for downstream APIs, waiting for queues, waiting for storage.</p>
<p>Virtual threads let you keep the readable request-per-thread programming model while dramatically reducing the cost of waiting.
They are not a replacement for good system design, sensible limits, or performance testing.
But for many Spring MVC applications running on Java 21 or later, <code>spring.threads.virtual.enabled=true</code> is now one of the most interesting properties worth testing.</p>
]]></content:encoded></item><item><title><![CDATA[Configure a background coroutine for your Spring Boot application]]></title><description><![CDATA[Sometimes you need to run work in the background of a Spring Boot application. This may be work that should not block the HTTP request that triggered it, or work that is started by a scheduled process]]></description><link>https://blog.hbrown.dev/configure-a-background-coroutine-for-your-spring-boot-application</link><guid isPermaLink="true">https://blog.hbrown.dev/configure-a-background-coroutine-for-your-spring-boot-application</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[coroutines]]></category><category><![CDATA[Springboot]]></category><dc:creator><![CDATA[Henry Brown]]></dc:creator><pubDate>Fri, 01 May 2026 07:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a6df6b110d8ecdf29cfd44b/f1ea3aba-2206-4dcd-95ca-c8147d3c2b6b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sometimes you need to run work in the background of a Spring Boot application. This may be work that should not block the HTTP request that triggered it, or work that is started by a scheduled process, event listener, message consumer, or application service.</p>
<p>In Kotlin, coroutines make it straightforward to start asynchronous work. However, I prefer not to scatter calls to <code>CoroutineScope(...)</code> or <code>Dispatchers.IO</code>, (and definitely <em>NOT</em> <code>GlobalScope</code>) throughout an application. Instead, I like to define an explicit background coroutine configuration and inject it where it is needed.</p>
<p>This gives the application a named place for background execution, error handling, and shutdown behaviour.</p>
<pre><code class="language-kotlin">@Configuration
@OptIn(ExperimentalCoroutinesApi::class)
class CoroutineConfig : DisposableBean {

    private val backgroundExecutor: ExecutorService = Executors.newVirtualThreadPerTaskExecutor()
    private val backgroundDispatcher = backgroundExecutor.asCoroutineDispatcher()

    private val handler = CoroutineExceptionHandler { ctx, e -&gt;
        val correlationId = ctx[CorrelationIdContextElement]?.correlationId ?: NO_CORRELATION_ID
        log.error("&gt;&gt;&gt; Unhandled background coroutine error, correlationId=[$correlationId]", ctx, e)
    }

    @Bean
    @Qualifier(BACKGROUND_DISPATCHER_NAME)
    fun backgroundDispatcher(): CoroutineDispatcher = backgroundDispatcher

    @Bean
    @Qualifier(BACKGROUND_SCOPE_NAME)
    fun backgroundScope(): CoroutineScope {
        return CoroutineScope(SupervisorJob() + handler + backgroundDispatcher + CoroutineName(BACKGROUND_SCOPE_NAME))
    }

    override fun destroy() {
        backgroundDispatcher.close()
        backgroundExecutor.shutdown()
    }

    companion object {
        private val log = CoroutineLogger(LoggerFactory.getLogger(CoroutineConfig::class.java))
    }
}

const val BACKGROUND_DISPATCHER_NAME = "backgroundDispatcher"
const val BACKGROUND_SCOPE_NAME = "backgroundScope"
private const val NO_CORRELATION_ID = "&lt;no correlation id&gt;"
</code></pre>
<h2>Why define a background coroutine configuration?</h2>
<p>The main reason is ownership.</p>
<p>When a coroutine is launched, something should own its lifecycle. In a web application, the request normally owns request-scoped work. If the request is cancelled, times out, or completes, that work should usually stop with it.</p>
<p>Background work is different. It often needs to continue independently of the request that caused it. For example, an endpoint may accept a command, persist the initial state, and then trigger some follow-up processing in the background. That follow-up processing should not be tied to the HTTP connection staying open.</p>
<p>By exposing a dedicated <code>CoroutineScope</code> as a Spring bean, you make that decision explicit:</p>
<pre><code class="language-kotlin">@Bean
@Qualifier(BACKGROUND_SCOPE_NAME)
fun backgroundScope(): CoroutineScope {
    return CoroutineScope(SupervisorJob() + handler + backgroundDispatcher + CoroutineName(BACKGROUND_SCOPE_NAME))
}
</code></pre>
<p>Any class that needs to launch background work can inject this scope using the qualifier. That keeps the application honest about which work is request-bound and which work is intentionally detached.</p>
<h2>Why use a dedicated dispatcher?</h2>
<p>The configuration creates an <code>ExecutorService</code> and adapts it into a coroutine dispatcher:</p>
<pre><code class="language-kotlin">private val backgroundExecutor: ExecutorService = Executors.newVirtualThreadPerTaskExecutor()
private val backgroundDispatcher = backgroundExecutor.asCoroutineDispatcher()
</code></pre>
<p>In this case, the executor uses Java virtual threads. That makes the dispatcher a good fit for many typical application-level background jobs. Especially where the job may spend time waiting on blocking I/O such as database calls, HTTP calls, file operations, or SDK clients that are not coroutine-native.</p>
<p>The important point is not only that this uses virtual threads. The important point is that background work is not silently being pushed onto an unrelated dispatcher. It has a specific execution policy that can be named, tested, monitored, and changed later without hunting through the codebase.</p>
<p>For example, if you later decide that a particular application needs a bounded thread pool, a different executor, or more detailed instrumentation, the change can happen in this configuration class rather than at every call site.</p>
<h2>Why use a SupervisorJob?</h2>
<p>The scope uses a <code>SupervisorJob</code>:</p>
<pre><code class="language-kotlin">CoroutineScope(SupervisorJob() + handler + backgroundDispatcher + CoroutineName(BACKGROUND_SCOPE_NAME))
</code></pre>
<p>This is useful for background processing because one failing child coroutine should not necessarily cancel every other background task running in the same scope.</p>
<p>For example, if three independent notification jobs are launched and one fails because a downstream provider is unavailable, the other two should usually be allowed to continue. <code>SupervisorJob</code> gives you that isolation.</p>
<p>This does not mean failures are ignored. It means failures are contained. The exception handler is still important.</p>
<h2>Centralised exception handling</h2>
<p>Background work can fail after the original caller has already received a response. That means you cannot rely on normal request handling to surface errors.</p>
<p>This configuration adds a <code>CoroutineExceptionHandler</code>:</p>
<pre><code class="language-kotlin">private val handler = CoroutineExceptionHandler { ctx, e -&gt;
    val correlationId = ctx[CorrelationIdContextElement]?.correlationId ?: NO_CORRELATION_ID
    log.error("&gt;&gt;&gt; Unhandled background coroutine error, correlationId=[$correlationId]", ctx, e)
}
</code></pre>
<p>The useful part here is that the error handling is not an afterthought at each launch site. Unhandled background coroutine failures are logged in one place, and the log entry attempts to include a correlation id from the coroutine context.</p>
<p>That is especially valuable when the background job was triggered by a request, message, or scheduled process, and you want to connect the failure back to the original flow.</p>
<h2>Clean shutdown</h2>
<p>The configuration implements <code>DisposableBean</code>:</p>
<pre><code class="language-kotlin">override fun destroy() {
    backgroundDispatcher.close()
    backgroundExecutor.shutdown()
}
</code></pre>
<p>The dispatcher is backed by an executor, and the executor should be shut down when the Spring application context is closed. Without this, you risk leaving executor resources alive longer than intended during shutdown, tests, redeployments, or local development restarts.</p>
<h2>Typical use cases</h2>
<p>I would typically use a background dispatcher like this for work that is application-owned, asynchronous, and not part of the direct response path.</p>
<p>Some examples are:</p>
<ol>
<li><p>Sending email, SMS, push notifications, or webhooks after a command has been accepted.</p>
</li>
<li><p>Calling third-party APIs where the user does not need to wait for the result immediately.</p>
</li>
<li><p>Post-processing uploaded files, generated reports, images, or exported data.</p>
</li>
<li><p>Updating derived state, caches, search indexes, or read models after a write.</p>
</li>
<li><p>Running fan-out tasks where one action needs to trigger several independent downstream operations.</p>
</li>
<li><p>Starting work from a Spring event listener or scheduled job while keeping the coroutine execution policy consistent.</p>
</li>
<li><p>Running blocking I/O integrations from coroutine-based services without mixing that concern into every service method.</p>
</li>
</ol>
<p>The common theme is that the work should be visible as background work in the code. Injecting a specifically named scope or dispatcher makes that intent clear.</p>
<h2>How it might be used</h2>
<p>A service can inject the scope and launch work on it:</p>
<pre><code class="language-kotlin">@Service
class NotificationService(
    @Qualifier(BACKGROUND_SCOPE_NAME)
    private val backgroundScope: CoroutineScope,
) {

    fun sendWelcomeNotification(command: WelcomeNotificationCommand) {
        backgroundScope.launch {
            sendEmail(command)
            sendSms(command)
        }
    }
}
</code></pre>
<p>If the launched coroutine fails unexpectedly, the exception handler configured on the scope will log the failure. If multiple jobs are running in the same background scope, <code>SupervisorJob</code> prevents one failing job from cancelling the entire scope.</p>
<h2>When not to use it</h2>
<p>This configuration is not a replacement for every asynchronous processing pattern.</p>
<p>If the work must be durable, retryable, or guaranteed to run after the application restarts, then a queue, database-backed job table, workflow engine, or message broker is usually a better fit. A coroutine launched in memory is still in-memory work. If the process dies, that work is gone.</p>
<p>I would also avoid using this for request-bound concurrency. If the work is part of producing the HTTP response, prefer structured concurrency inside the suspend function handling the request. That way cancellation and error propagation remain tied to the request lifecycle.</p>
<p>For me, this configuration is the middle ground: useful for application-owned background work that should be explicit, observable, and cleaned up correctly, but does not need the durability guarantees of a full job processing system.</p>
]]></content:encoded></item></channel></rss>