DEV Community

Cover image for Kotlin Virtual Threads Without the Magic: ABCoroutines for Kotlin
Rob D
Rob D

Posted on • Originally published at robd.tech

Kotlin Virtual Threads Without the Magic: ABCoroutines for Kotlin

The Opportunity

Virtual Threads arrived with JDK 21 — promising lightweight concurrency without frameworks. But while they simplify threading, they don’t give you structure. You still have to manage scopes, cancellations, timeouts, and races yourself.

That’s why I built ABCoroutines — a small, explicit toolkit that turns JDK 21 Virtual Threads into a structured-concurrency experience for Kotlin developers.

💡 The Real Problem

Virtual Threads are threads, not coroutines — they don’t compose naturally.
Existing frameworks hide too much behind magic or global context.
Most examples focus on “Hello Virtual Thread”, not real coordination patterns. Memory leaks are common.
Kotlin already has a great coroutine model, but it doesn’t automatically map onto JDK Virtual Threads. ABCoroutines bridges that gap.

⚙️ Introducing ABCoroutines

🧩 Structured concurrency built on JDK 21 Virtual Threads — without the magic.

It’s a minimal, composable toolkit offering:

A VirtualThreads CoroutineDispatcher (backed by Executors.newVirtualThreadPerTaskExecutor())
A long-lived applicationScope
Predictable lifecycle management (ensureRunning, shutdown, reset)
High-level coordination patterns: parallel, zip, raceForSuccess, retry
Clean timeout and retry wrappers using Kotlin’s Duration
Java interop through standard Executor / ExecutorService
74 tests verifying cancellation, resource safety, and concurrency

🧩 Relationship to JCoroutines

ABCoroutines builds on concepts proven in my earlier project, JCoroutines — a pure Java 21 implementation of structured concurrency designed around Virtual Threads.
While JCoroutines brings coroutine-like structure to Java itself, ABCoroutines adapts those same principles to Kotlin — combining idiomatic suspend functions with the predictability and lifecycle control of Virtual Threads.

To keep things focused, this release includes only minimal interop:
it exposes the underlying Virtual Thread Executor through a standard Executor and ExecutorService, so Java code can safely schedule work into the same structured environment.

A more complete JCoroutines ↔ ABCoroutines interop layer is currently being prepared for release.
It’s a fascinating area — particularly for testing and mixed Java/Kotlin projects, where being able to share structured scopes and cancellation semantics across both languages opens up new possibilities for gradual migration and hybrid systems.

🧠 Thinking in Patterns, Not Primitives

Instead of juggling thread pools, you express intent:

Parallel Execution

val results = parallel(
    { fetchUserProfile(userId) },
    { fetchUserOrders(userId) },
    { fetchUserPreferences(userId) }
)
Enter fullscreen mode Exit fullscreen mode

All three operations run concurrently on virtual threads. If any fails, the others are cancelled.

Racing for the Fastest Result

val quote = raceForSuccess(
    { fetchQuoteFromProvider1() },
    { fetchQuoteFromProvider2() },
    { fetchQuoteFromProvider3() }
)
Enter fullscreen mode Exit fullscreen mode

Returns the first successful result, cancelling the slower operations.

Combining Results

val (profile, orders) = zip(
    { fetchUserProfile(userId) },
    { fetchUserOrders(userId) }
)
Enter fullscreen mode Exit fullscreen mode

Wait for both results, but cancel everything if either fails.

Retry with Exponential Backoff

val data = retry(
    maxAttempts = 3,
    initialDelay = 100.milliseconds,
    factor = 2.0
) {
    fetchFromUnreliableApi()
}
Enter fullscreen mode Exit fullscreen mode

Automatically retries failed operations with configurable backoff.

Timeouts

val result = withTimeout(5.seconds) {
    slowBlockingOperation()
}
Enter fullscreen mode Exit fullscreen mode

Clean timeout handling with proper cancellation.

🏗️ Lifecycle Management

ABCoroutines provides explicit lifecycle control:

// Application startup
ABCoroutines.ensureRunning()

// Long-running background task
applicationScope.launch {
    while (isActive) {
        processQueue()
        delay(1.minutes)
    }
}

// Graceful shutdown
ABCoroutines.shutdown(timeout = 30.seconds)
Enter fullscreen mode Exit fullscreen mode

No hidden state, no global magic — just predictable behavior.

🔄 Java Interoperability

Need to integrate with Java code?

val executor: ExecutorService = ABCoroutines.asExecutorService()
Enter fullscreen mode Exit fullscreen mode

// Pass to Java libraries expecting ExecutorService
javaLibrary.setExecutor(executor)
Virtual threads work seamlessly with existing Java concurrency APIs.

🎯 Real-World Use Cases
Web Server Request Handling

applicationScope.launch(VirtualThreads) {
    val (user, permissions, settings) = parallel(
        { userRepository.findById(userId) },
        { permissionService.getPermissions(userId) },
        { settingsRepository.getSettings(userId) }
    )

    buildResponse(user, permissions, settings)
}
Enter fullscreen mode Exit fullscreen mode

Database Connection Pool Integration

// Run blocking JDBC calls on virtual threads

suspend fun <T> withConnection(block: (Connection) -> T): T {
return withContext(VirtualThreads) {
dataSource.connection.use { conn ->
block(conn)
}
}
}
External API Integration with Fallbacks

val data = raceForSuccess(
    { primaryApi.fetch() },
    { 
        delay(500.milliseconds)
        secondaryApi.fetch() 
    }
)
Enter fullscreen mode Exit fullscreen mode

Try the primary API, but switch to secondary if it’s too slow.

📊 Why Virtual Threads?
Virtual threads shine when you have:

Many concurrent blocking operations (database queries, file I/O, HTTP calls)
Legacy blocking code that can’t easily be converted to async
Simpler mental model than async/await for I/O-bound work
ABCoroutines gives you virtual threads with the structured concurrency guarantees you expect from Kotlin.

🚀 Getting Started
Available on Maven Central:

dependencies {
    implementation("tech.robd:abcoroutines:0.1.0")
}
Enter fullscreen mode Exit fullscreen mode

Requirements:

Kotlin 2.0+
Java 21 or later
kotlinx.coroutines
Quick Start:

import tech.robd.abcoroutines.*

fun main() {
    ABCoroutines.ensureRunning()

    runBlocking {
        val result = withContext(VirtualThreads) {
            // Your blocking code here
            performBlockingOperation()
        }
        println("Result: $result")
    }

    ABCoroutines.shutdown()
}
Enter fullscreen mode Exit fullscreen mode

🎓 Design Philosophy
ABCoroutines follows three principles:

Explicit over Implicit: No hidden global state or context injection
Composable Primitives: Build complex patterns from simple building blocks
Fail-Safe Defaults: Proper cancellation and resource cleanup by default
🔗 Learn More
GitHub: github.com/robdeas/abcoroutines
Documentation: Full examples and API docs in the README
Maven Central: central.sonatype.com/artifact/tech.robd/abcoroutines
💭 Final Thoughts
Virtual Threads are powerful, but raw threads aren’t enough. ABCoroutines gives you the structure you need without sacrificing transparency.

If you’re building JVM server applications with blocking I/O, give it a try. It’s designed to be small, clear, and composable — no magic required.

Try ABCoroutines today and let me know what you think! Issues, suggestions, and contributions are welcome on GitHub.

Top comments (0)