Javable: generate Java-friendly wrappers for Kotlin with KSP

Table of Contents
TL;DR: Javable is a Kotilin Symbol Processing (KSP) processor that generates Java-friendly wrappers for Kotlin classes. Annotate your class with @JavaApi and your functions with @AsyncJavaApi or @BlockingJavaApi, and KSP generates CompletableFuture-based async adapters, blocking wrappers, and Stream-based Flow collectors — with correct CoroutineScope lifecycle management.
Imagine you are a Kotlin library developer. You have created a beautiful API and want to conquer the JVM world. But then you realize that everyone is still using Java.
When you look at your API from a Java developer’s point of view, it no longer looks as clean:
- Data classes with 10+ parameters. Without Kotlin named parameters, calling them from Java becomes a mess. You need fluent builders.
- Methods returning
Flow. TheFlowtype is a Kotlin Coroutines concept — Java has no native equivalent. - Suspending functions that read like prose in Kotlin but surface as ugly methods with a raw
$continuationparameter in Java.
You start writing Java-specific extensions using Consumer callbacks and CompletableFuture, wrapping async code with runBlocking { } and GlobalScope.future { /* suspending function call */ }. But subtle bugs follow: using GlobalScope violates structured concurrency and silently drops exceptions.
If you handle structured concurrency correctly, you still end up with a lot of repetitive boilerplate. Even if an AI agent writes it quickly — there is an established pattern in the repo — it is still non-deterministic, and AI can make mistakes.
I faced this problem and created Javable — a small KSP processor that generates Java-friendly wrappers for Kotlin classes and functions.
How it works #
Javable uses three annotations:
@JavaApi— placed on the class. Controls whether a Java wrapper (*Java.java), a Kotlin wrapper (*Kotlin.kt), or both should be generated, and whether the wrapper should implementAutoCloseable.@AsyncJavaApi— placed onsuspendfunctions or functions returningFlow<T>. Generates aCompletableFuture<T>,CompletionStage<T>, or blockingStream<T>method.@BlockingJavaApi— placed onsuspendfunctions. Generates a plain synchronous method viarunBlocking, suitable for dedicated worker threads or Java 21 virtual threads.
Non-suspend public functions are forwarded unchanged in both wrappers.
Setup #
Add the KSP plugin and Javable dependencies to your build.gradle.kts:
plugins {
id("com.google.devtools.ksp") version "[LATEST VERSION]"
}
dependencies {
implementation("me.kpavlov.javable:javable-annotations:[LATEST VERSION]")
ksp("me.kpavlov.javable:javable-ksp:[LATEST VERSION]")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.10.2")
}
Javable is not yet on Maven Central. Build and publish locally first:
./gradlew publishToMavenLocal, then addmavenLocal()to your repositories.
Annotating a class #
Here is a minimal example:
@JavaApi(javaWrapper = true, autoCloseable = true)
class Calculator {
@AsyncJavaApi
suspend fun add(a: Int, b: Int): Int {
delay(10L)
return a + b
}
@BlockingJavaApi
suspend fun multiply(a: Int, b: Int): Int {
delay(10L)
return a * b
}
}
KSP generates CalculatorJava.java with:
add(int a, int b): CompletableFuture<Integer>— uses the wrapper’s built-inCoroutineScopeadd(int a, int b, Executor executor): CompletableFuture<Integer>— runs on the caller-supplied executormultiply(int a, int b): int throws InterruptedException— synchronous, blocks the calling thread
From Java:
try (var calc = new CalculatorJava()) {
int result = calc.multiply(3, 4); // blocking
calc.add(3, 4).thenAccept(System.out::println); // async
}
Because autoCloseable = true, the wrapper implements AutoCloseable and can be used in a try-with-resources block. Closing it cancels the internal CoroutineScope and waits for all child coroutines to finish.
Exposing Flow as Stream #
For functions returning Flow<T>, use @AsyncJavaApi(wrapperType = JavaWrapperType.STREAM):
@JavaApi(javaWrapper = true)
class EventSource {
@AsyncJavaApi(wrapperType = JavaWrapperType.STREAM)
fun events(): Flow<String> = flow {
emit("started")
delay(100L)
emit("done")
}
}
The generated method returns Stream<String> and collects the entire flow with runBlocking before returning. This buffers all elements in memory — for large or infinite flows, a reactive adapter (Flux, Publisher) is planned for a future release.
Annotation reference #
@JavaApi (class level) #
| Parameter | Default | Effect |
|---|---|---|
kotlinWrapper | true | Generate *Kotlin.kt |
javaWrapper | false | Generate *Java.java |
autoCloseable | false | Java wrapper implements AutoCloseable |
@AsyncJavaApi (function level) #
wrapperType | Applies to | Generated return type |
|---|---|---|
COMPLETABLE_FUTURE (default) | suspend functions | CompletableFuture<T> |
COMPLETION_STAGE | suspend functions | CompletionStage<T> |
STREAM | fun or suspend fun returning Flow<T> | Stream<T> (blocking collect) |
For COMPLETABLE_FUTURE and COMPLETION_STAGE, two overloads are generated: one using the wrapper’s built-in scope, one accepting a caller-supplied Executor. For STREAM, a single blocking method with no executor overload.
@BlockingJavaApi (function level) #
Wraps a suspend function as a plain synchronous call via runBlocking. The generated method declares throws InterruptedException. If both @AsyncJavaApi and @BlockingJavaApi are placed on the same function, @AsyncJavaApi takes precedence.
Scope and resource management #
When the wrapper holds a CoroutineScope, close() cancels the scope and waits for all child coroutines to finish. The Kotlin wrapper uses runBlocking { job?.join() }; the Java wrapper does the same with a 5-second timeout and surfaces failures rather than swallowing them.
Whether a scope is created depends on the annotations used. A Java wrapper gets a scope whenever it has at least one @AsyncJavaApi method with COMPLETABLE_FUTURE or COMPLETION_STAGE return type; STREAM and @BlockingJavaApi methods don’t need one. Setting autoCloseable = true goes one step further and makes the Java wrapper implement AutoCloseable, so the scope is cleaned up explicitly by the caller. A Kotlin wrapper follows the same rule — scope is present only when there are Future/Stage methods — and always implements AutoCloseable in that case.
Current status #
Javable is at 0.1.0-SNAPSHOT and not yet on Maven Central. Check the source code, file issues, or contribute. Reactive adapter support for infinite Flow streams is on the roadmap.
Is Kotlin–Java interop a pain point on your team? I’d be curious to hear what approach you use today.