This document is the written stability contract for Kdrant: what "stable" means, what changes are allowed in which releases, and what a major bump costs you. It complements the board (where the project is going) and the CHANGELOG (what has already shipped).
Kdrant follows Semantic Versioning 2.0.0. Given MAJOR.MINOR.PATCH:
- MAJOR: incompatible public-API changes.
- MINOR: backwards-compatible additions (new operations, new optional parameters, new overloads).
- PATCH: backwards-compatible bug fixes.
The public API is exactly what the binary-compatibility-validator
tracks in the committed dump files: a *.api file per module for the JVM, and from 2.2.0 a
*.klib.api beside it for every module that publishes native targets. The gRPC module's generated
protobuf and stub classes are excluded, because their surface is Qdrant's to change and not ours to
promise. Every public and protected symbol is in those dumps; ./gradlew apiCheck fails the build on
any untracked change, so API breakage is never silent. Anything not in them, meaning internal
declarations and symbols annotated @InternalKdrantApi, is not public API and may change at any time.
The klib dump exists because the JVM one was a statement about a single target. A change to a
commonMain declaration that only Kotlin/Native can see used to produce no diff at all: an expect
gaining a parameter, an actual narrowing something, a declaration moving from commonMain into
jvmMain and quietly disappearing from every other artifact. For a consumer on iosArm64 this
sentence was, until 2.2.0, a promise about a file that did not describe their artifact.
The wire behaviour of an engine (the requests it sends and the responses it parses) is also part of
the contract: a change that alters what goes on the wire for an existing operation is treated as a
breaking change unless it is a bug fix bringing Kdrant in line with Qdrant's documented API. Contract
tests validate every request body the REST engine builds against Qdrant's own OpenAPI document, pinned
to the version the CI matrix runs against, and a shared behavioural suite runs both engines against a
real Qdrant, so a wire change is a failing build rather than a silent difference. From 2.1.0 that
suite also runs from a Linux and a macOS native binary, because an engine that has only ever been
exercised from a JVM has been shown to link rather than to work.
The two engines are held to the same behaviour, with one stated exception. Qdrant serves fourteen
operations over HTTP only: telemetry, Prometheus metrics, the two issues calls, recoverSnapshot, the
snapshot and storage-snapshot transfers, and the six shard-scope snapshot operations. On the gRPC engine each
throws an UnsupportedOperationException naming itself and naming REST. That list is part of this
contract: an operation leaving it is an additive change, and an operation joining it would be a
breaking one.
Two Kotlin details are additive to apiCheck but not binary-compatible for every caller, and it is
better to say so than to discover it later.
QdrantClient and QdrantTransport are interfaces you call, not interfaces you implement. Kdrant
adds operations to both as Qdrant grows, and a new member on a Kotlin interface breaks a class that
implemented it against an older jar. If you need a decorator, delegate to the interface (by transport)
so the compiler tells you what a new release added; if you need a stub in a test, generate it. A
third-party wire engine is a supported use of QdrantTransport, but it is a use that recompiles against
each minor.
Adding a field to a public data class changes its generated copy and componentN. New response
fields arrive as Qdrant returns more, and while the constructor keeps its defaults and source keeps
compiling, code that called copy() against an older jar needs recompiling. Kdrant does not add fields
gratuitously and each one is listed in the CHANGELOG, but a minor upgrade is a
recompile, not a jar swap.
The same holds for a new optional parameter on a public constructor, and KdrantConfig is the one it
happens to: a defaulted parameter appended to it is source-additive and changes the constructor
signature Kotlin emits, so code that called the constructor positionally against an older jar needs
recompiling. The configuration DSL, Kdrant(host, port) { ... }, which is the documented way in, is
unaffected, and that is why the parameter goes on the end rather than beside the one it belongs with.
A klib is not a jar, and a promise that does not say so is a promise a consumer on linuxX64 cannot
check. The rules below are what a minor may do to each kind of artifact this repository publishes.
| Artifact | A minor may | A minor may not |
|---|---|---|
JVM jars (kdrant-core-jvm, both engines, the adapters) |
add declarations; add a defaulted parameter to a public constructor or function, which is source-compatible and needs a recompile; add a field to a public data class, with the same caveat |
remove or rename a declaration; change a return type or a parameter type; move a declaration between packages |
klibs (kdrant-core, kdrant-transport-rest, kdrant-migrate, per native target) |
add declarations to every target at once; add a target | remove a declaration from any target it already had, including where the JVM keeps it; narrow a declaration to fewer targets; drop a target |
| The BOM | add a module; move a version forward | remove a module already listed |
| The CLI binaries | change flags additively; add a subcommand | remove a subcommand or a flag without a deprecation in a prior minor |
Three things about the klib row are worth stating rather than leaving to be discovered.
Narrowing counts as a removal. A declaration that exists on eight targets and is changed to exist
on seven has been removed for whoever was on the eighth, and the merged dump shows exactly that: the
target annotation on the line changes. apiCheck fails on it, which is the point of the file being
merged rather than one dump per target.
The Kotlin compiler participates. Kotlin/Native's binary compatibility is not the JVM's, and a
klib is linked against the compiler version that produced it. Kdrant does not promise that a klib built
by one Kotlin version links against a consumer built by an older one; that is Kotlin's guarantee to
make, not this project's. What is promised is that a Kotlin toolchain upgrade inside a 2.x minor is
recorded in the CHANGELOG under Internal, so an upgrade that forces a rebuild is
never a surprise.
A Qdrant change that forces a signature change is still a break. If Qdrant alters an existing endpoint in a way that cannot be carried additively, Kdrant does not quietly change the signature in a minor. The old shape stays and is deprecated, the new one arrives beside it, and the removal waits for the next major, on the same policy as everything else here.
Within a major version:
- No breaking public-API change without a major bump. Source and binary compatibility are maintained across a major version, for jars and for klibs, on the terms above.
- Deprecation policy. An API is deprecated with
@Deprecated(with aReplaceWithwhere possible) for at least one minor release before removal, and removal happens only in a major release. - Coroutine contract. Every operation stays a
suspendfunction or aFlow; cancellation is cooperative andCancellationExceptionis always propagated. - Wire compatibility. Kdrant tracks Qdrant's stable API; new Qdrant features arrive as additive minor releases.
2.1.0 is a minor and every 2.0.0 call site compiles unchanged. Two things are worth knowing before
the jar is swapped rather than the build re-run.
kdrant-transport-rest is Kotlin Multiplatform now, so its JVM classes are published as
kdrant-transport-rest-jvm and the plain coordinate carries Gradle module metadata. This is the move
kdrant-core made at 2.0.0, with the same consequence: a Gradle build reads the metadata and changes
only the version number, and a Maven build naming kdrant-transport-rest gets no classes and has to
name kdrant-transport-rest-jvm. The gRPC engine and the framework adapters are JVM-only and are
unaffected.
Three signatures changed shape. Kdrant(...) and KdrantGrpc(...) gained decorateTransport, and
KdrantConfig gained bearerToken. All three parameters are optional and every 2.0.0 call site
compiles unchanged, but a default parameter changes the signature Kotlin emits, so an application
compiled against 2.0.0 that swaps in the 2.1.0 jar without rebuilding will not find them. That is
the case above, and it now reaches the entry point rather than a
data class, which is worth stating plainly: 2.1.0 is a rebuild, not a jar swap. Running
git diff v2.0.0 v2.1.0 -- '*/api/*.api' shows the seven removed lines and nothing else removed.
Two behaviours changed without any API changing. HTTP 403 and gRPC PERMISSION_DENIED now raise
KdrantException.Forbidden rather than Unauthorized; Forbidden is a subclass, so an existing
catch still catches it and a when over the hierarchy stays exhaustive, but a when that branched
on Unauthorized to mean "check the API key" now also sees a scoped token being refused. And a
credential is no longer rejected over plaintext HTTP when the host is a loopback address, which only
accepts configurations that were previously refused.
2.0.0 breaks two things, and source compatibility is not one of them: every 1.x call site compiles
unchanged.
The first is where kdrant-core's JVM classes are published. The module is Kotlin Multiplatform now, so its own
coordinate carries Gradle module metadata and the classes live in kdrant-core-jvm. Gradle reads that
metadata and resolves the variant, so a Gradle build changes nothing but the version number. Maven does
not read it, so a Maven build naming kdrant-core gets no classes and must move to kdrant-core-jvm.
Depending on kdrant-transport-rest or kdrant-transport-grpc, which is what the README recommends, is
unaffected either way.
The second is that ScrollRequest and SearchRequest gained a shardKey parameter. That is the case
above arriving for real: the constructor keeps its defaults and
source keeps compiling, but the generated copy and componentN changed, so code that called copy()
on either type against a 1.x jar has to be recompiled. Nothing else in the *.api dump was removed.
The multiplatform migration itself changed no public API. The dump is identical either side of it, which is worth stating because it is the part that sounds like it should have broken something. What broke is the two lines above.
The rest of 2.0.0 is additive: the gRPC engine is a module you do not have yet, and cluster support,
formula and MMR reranking, shard-scope snapshots and the Koog module are new operations.
Kdrant is deliberately Kotlin-coroutine-first, which is the wedge (see the README). The
public API is suspend functions and Flows, which are callable from Java but not idiomatic there.
There is no bundled CompletableFuture facade. Mirroring ~40 suspend operations into a
blocking or future-returning Java API is a large, duplicated surface to maintain, and it is not the audience
Kdrant optimises for. Java callers who need it should bridge with the standard tools:
kotlinx-coroutines-jdk8'sfuture { }to turn asuspendcall into aCompletableFuture, orrunBlocking { }for a simple synchronous call.
A dedicated kdrant-jdk facade remains an on-demand option if there is real Java demand; it would be
additive and would not change the Kotlin API.