Kotlin uses the same runtime, agent, annotations, and coordinator binding as the Java SDK — read that page for interception, CoordinatorBinding, control annotations, and troubleshooting. What differs is the compile-time half: Kotlin code is processed by a KSP plugin, io.varianz.sdk.kotlin, not the Java annotation processor.
Supported versions
Kotlin 2.3.x is the floor, and it is not optional. The KSP processor is built against Kotlin
2.4. On Kotlin 2.1.x every build fails with:Kotlin 2.2.x compiles the main source set but cannot use varianz-kotest: the whole test source
set fails with metadata is 2.4.0, expected version is 2.2.0, down to Unresolved reference 'println'.Gradle must also be new enough for your JDK — Gradle 8.x cannot run on JDK 24 or 25. See
Installation → Java.
Setup
Six things trip up first-time setup:
- The plugin id is
io.varianz.sdk.kotlin, not io.varianz.sdk.
- Give the plugin a version in
plugins {}, or pluginManagement must supply one — otherwise Gradle reports Plugin [id: 'io.varianz.sdk.kotlin'] was not found.
- Apply
kotlin("jvm") and KSP first, in that order — the Varianz plugin throws NoClassDefFoundError: …KotlinCompilerPluginSupportPlugin without the Kotlin plugin, and throws outright unless com.google.devtools.ksp precedes it.
- Declare a project-level
repositories {} block as well as pluginManagement — without it, io.varianz:kotlin-processor cannot resolve.
- Add JavaParser to
ksp yourself — without it the processor dies with NoClassDefFoundError: com/github/javaparser/ast/Node.
- Configure via the typed extension (
configure<VarianzKotlinExtension>) — the generated varianz {} accessor isn’t available when the plugin is applied dynamically.
The pluginManagement repository setup is the same as Java’s.
Plugin options
On VarianzKotlinExtension:
Defining VPoints
- Annotation attributes require named arguments:
@VPoint(name = "…"), never positional.
- Use a
data class (or annotate with @Struct) for every type in a @VPoint signature. An ordinary Kotlin class is not auto-derived even with val properties, and the VPoint then fails at class-init with binding hints that reference unregistered structs. val and var are modelled identically — both read via getter and constructed via the primary constructor.
- Field names are used verbatim, in camelCase:
Money { currencyCode: … }, not currency_code. This differs from the Java annotation processor, which converts camelCase to snake_case — the two processors do not agree, so a CEL literal written for a Java DTO will not apply to the Kotlin equivalent.
Not supported in @VPoint signatures
Two Kotlin features are rejected at compile time with clear errors:
@JvmInline value class parameters or returns
suspend fun — wrap the suspending call in a plain function and instrument that
These compile with no warning and then fail. Avoid them until they are fixed.The last row is the dangerous one: one unsupported type in one signature takes out the whole class,
and only with VARIANZ_ENABLED=true, so a disabled build looks fine.
Adding a new Kotlin file breaks the next incremental build. kspKotlin regenerates only the
changed file’s aggregate and deletes the rest, so you get either
Unresolved reference 'PricingServiceVPoints' — which persists across rebuilds until you
gradle clean — or, worse, a successful build whose jar is missing most of its VPoints. Run
gradle clean build after adding a file that declares a @VPoint.
Runtime and deployment
Everything on the Java page applies: the varianz-agent must be attached (automatic for Gradle test/run, explicit for production launchers), CoordinatorBinding.init(...) + <Class>VPoints.bind(instance) at startup, and --enable-native-access=io.varianz.native_loader on Java 22+.
A plain ThreadLocal is not enough for coroutines. VarianzSessionBinding stores the session
id in a ThreadLocal, which is not restored on a dispatcher thread — so a @VPoint called inside
withContext(Dispatchers.IO) or async(Dispatchers.Default) silently never sees its stage, while
awaitSync still reports the subscriber as confirmed. No carrier ships for this yet.Carry it across the hop with a ThreadContextElement built on the three public methods
VarianzSessionBinding.set(String), .current() and .clear() (package io.varianz.grpc) —
updateThreadContext saves current() and sets the session id, restoreThreadContext puts the
saved value back. With OpenTelemetry, opentelemetry-extension-kotlin’s asContextElement() does
the same job for Baggage.
See Propagate sessions.
A top-level @VPoint generates a <File>KtVPoints aggregate whose bind() takes the Kotlin file
facade class and therefore cannot be called from Kotlin source. Top-level VPoints do not need
bind() — loading the aggregate class is enough.
Testing
Kotlin tests use the JUnit 5 extension (see Testing → JUnit); a Kotest extension (io.varianz:varianz-kotest, added by includeKotest.set(true)) provides the same session lifecycle for Kotest specs. It is compiled against Kotest 6.x and does not link on Kotest 5.