You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adopt the Kotlin binary-compatibility-validator (BCV) as an automated gate over okapi's public/Java-facing API surface. We currently have no guard against a silently-dropped @Jvm* annotation (or any other public-API drift), which is exactly how #74 slipped in.
Motivation
In #74 the @JvmOverloads on OutboxProcessor's constructor was dropped, silently breaking new OutboxProcessor(store, entryProcessor) for Java callers. Nothing caught it — okapi has no Java-interop tests and no ABI validation; the only gates are ktlint + Kotest. #75 fixes that one class with a hand-written Java compile-test, but that guards a single type. We want a net over the whole @Jvm* surface (@JvmOverloads, @JvmName un-mangling value-class methods, @JvmStatic, @JvmInline value class OutboxId) across all published modules.
Research
We surveyed how mature Kotlin JVM libraries actually guard interop, cross-checked the tooling against primary docs, and verified the key claim on okapi's own code.
BCV is the de-facto ecosystem norm — 10/10 libraries surveyed use it (committed .api dumps + apiCheck in CI): okio, okhttp, kotlinx.coroutines, kotlinx.serialization, kotlinx-datetime, arrow, ktor, mockk, Exposed, koin. It catches a dropped @JvmName/@JvmStatic/@JvmOverloads because those change the recorded JVM signatures (method name, static-vs-instance, the set of overloads).
Hand-written Java interop tests are a minority, supplementary pattern (3/10: coroutines, okhttp, arrow) — sparse and targeted at interop-fragile spots (checked exceptions via @Throws, companion @JvmStatic, generics), not whole-surface mirroring. Not an anti-pattern, but not the primary net; several respected libraries (serialization, ktor, mockk, Exposed, koin) ship none and rely on BCV alone.
@IntroducedAt is the wrong tool here — it preserves binary compatibility for Kotlin callers when adding optional parameters, and is still experimental. okapi's concern is Java-callability, which is what @JvmOverloads is for. Keep @JvmOverloads.
Empirical proof on okapi's own code
Wired standalone BCV into a throwaway branch and ran apiDump — OutboxProcessor dumps three public constructors (the @JvmOverloads-generated ones):
public fun <init> (OutboxStore, OutboxEntryProcessor)V
public fun <init> (OutboxStore, OutboxEntryProcessor, OutboxProcessorListener)V
public fun <init> (OutboxStore, OutboxEntryProcessor, OutboxProcessorListener, Clock)V
Removing @JvmOverloads and running apiCheck fails the build with exactly the regression from #74:
API check failed for project okapi-core.
- public fun <init> (OutboxStore, OutboxEntryProcessor)V
- public fun <init> (OutboxStore, OutboxEntryProcessor, OutboxProcessorListener)V
public fun <init> (OutboxStore, OutboxEntryProcessor, OutboxProcessorListener, Clock)V
Setup was ~one-time: apply once at the root, apiDump (~12s), commit one .api per published module. apiCheck auto-wires into check, so existing CI (./gradlew check) gets it for free.
Proposal
Primary: adopt the standaloneorg.jetbrains.kotlinx.binary-compatibility-validator plugin (the same one every surveyed library uses), applied at the root, with apiValidation { ignoredProjects.addAll(listOf("okapi-integration-tests", "okapi-benchmarks", "okapi-bom")) }. Commit the .api dumps; intentional API changes then show up as a reviewable .api diff.
Not the built-in KGP abiValidation yet — it's still experimental (Kotlin 2.2.0+, requires @OptIn, DSL may change). Revisit once it stabilizes.
Keep the Java compile-test from fix: add @JvmOverloads to OutboxProcessor constructor #75 as belt-and-suspenders (same pattern as coroutines/okhttp/arrow) for the interop-fragile spots and as executable Java-usage documentation.
Honest limitations
BCV catches changes to the recorded API; it will bake a forgotten @JvmName on a brand-new method into the golden dump as "correct" (it only trips on later drift). The Java test has the symmetric gap (only covers what you write a call for). Neither is complete alone; together they cover the demonstrated failure mode well. Main ongoing cost: regenerating and committing the .api dump on every intentional public-API change — friction that doubles as a review signal for a library that promises interop stability.
Summary
Adopt the Kotlin binary-compatibility-validator (BCV) as an automated gate over okapi's public/Java-facing API surface. We currently have no guard against a silently-dropped
@Jvm*annotation (or any other public-API drift), which is exactly how #74 slipped in.Motivation
In #74 the
@JvmOverloadsonOutboxProcessor's constructor was dropped, silently breakingnew OutboxProcessor(store, entryProcessor)for Java callers. Nothing caught it — okapi has no Java-interop tests and no ABI validation; the only gates are ktlint + Kotest. #75 fixes that one class with a hand-written Java compile-test, but that guards a single type. We want a net over the whole@Jvm*surface (@JvmOverloads,@JvmNameun-mangling value-class methods,@JvmStatic,@JvmInline value class OutboxId) across all published modules.Research
We surveyed how mature Kotlin JVM libraries actually guard interop, cross-checked the tooling against primary docs, and verified the key claim on okapi's own code.
BCV is the de-facto ecosystem norm — 10/10 libraries surveyed use it (committed
.apidumps +apiCheckin CI): okio, okhttp, kotlinx.coroutines, kotlinx.serialization, kotlinx-datetime, arrow, ktor, mockk, Exposed, koin. It catches a dropped@JvmName/@JvmStatic/@JvmOverloadsbecause those change the recorded JVM signatures (method name, static-vs-instance, the set of overloads).Hand-written Java interop tests are a minority, supplementary pattern (3/10: coroutines, okhttp, arrow) — sparse and targeted at interop-fragile spots (checked exceptions via
@Throws, companion@JvmStatic, generics), not whole-surface mirroring. Not an anti-pattern, but not the primary net; several respected libraries (serialization, ktor, mockk, Exposed, koin) ship none and rely on BCV alone.@IntroducedAtis the wrong tool here — it preserves binary compatibility for Kotlin callers when adding optional parameters, and is still experimental. okapi's concern is Java-callability, which is what@JvmOverloadsis for. Keep@JvmOverloads.Empirical proof on okapi's own code
Wired standalone BCV into a throwaway branch and ran
apiDump—OutboxProcessordumps three public constructors (the@JvmOverloads-generated ones):Removing
@JvmOverloadsand runningapiCheckfails the build with exactly the regression from #74:Setup was ~one-time: apply once at the root,
apiDump(~12s), commit one.apiper published module.apiCheckauto-wires intocheck, so existing CI (./gradlew check) gets it for free.Proposal
org.jetbrains.kotlinx.binary-compatibility-validatorplugin (the same one every surveyed library uses), applied at the root, withapiValidation { ignoredProjects.addAll(listOf("okapi-integration-tests", "okapi-benchmarks", "okapi-bom")) }. Commit the.apidumps; intentional API changes then show up as a reviewable.apidiff.abiValidationyet — it's still experimental (Kotlin 2.2.0+, requires@OptIn, DSL may change). Revisit once it stabilizes.Honest limitations
BCV catches changes to the recorded API; it will bake a forgotten
@JvmNameon a brand-new method into the golden dump as "correct" (it only trips on later drift). The Java test has the symmetric gap (only covers what you write a call for). Neither is complete alone; together they cover the demonstrated failure mode well. Main ongoing cost: regenerating and committing the.apidump on every intentional public-API change — friction that doubles as a review signal for a library that promises interop stability.Sources
@JvmOverloadsoverload dumping.api+ Java interop test · coroutines Java@Throwsguard · arrow@JvmStaticJava guardRelated: #74, #75.