The Problem 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) }
)

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() }
)

Returns the first successful result, cancelling the slower operations.

Combining Results

val (profile, orders) = zip(
    { fetchUserProfile(userId) },
    { fetchUserOrders(userId) }
)

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()
}

Automatically retries failed operations with configurable backoff.

Timeouts

val result = withTimeout(5.seconds) {
    slowBlockingOperation()
}

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)

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

🔄 Java Interoperability

Need to integrate with Java code?

val executor: ExecutorService = ABCoroutines.asExecutorService()

// 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)
}

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() 
    }
)

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")
}

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()
}

🎓 Design Philosophy

ABCoroutines follows three principles:

  1. Explicit over Implicit: No hidden global state or context injection
  2. Composable Primitives: Build complex patterns from simple building blocks
  3. Fail-Safe Defaults: Proper cancellation and resource cleanup by default

🔗 Learn More

💭 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.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Scroll to Top