FAQ
Straight answers, not marketing copy. If something genuinely isn't verifiable from the source or docs, that's stated plainly below instead of guessed at.
Why not Hibernate / Spring Data Cassandra / Exposed?
Because they start from the wrong question. A relational ORM's whole job is to let you write
findByEmail(email) against any column and have the database figure out how — that's the
mental model an ORM exists to serve, and it's a mental model built for a database where "add an
index and the query just works" is basically true.
Cassandra/ScyllaDB doesn't work that way, and no ORM can paper over that without lying to you. Every table is designed around the specific queries it needs to serve — "query-first modeling" — and a query against a column that isn't the partition key, a clustering key, or an explicitly indexed column doesn't get slower, it becomes a cluster-wide scatter-gather scan. An ORM whose mental model is "hide the database, expose an object graph" hides exactly the fact you most need to see: that "find X by Y" on a non-key column is either a table you haven't built yet or a scatter-gather query you're choosing on purpose.
Kandra's mental model is the opposite: partition-oriented modeling, surfaced in the type system,
not hidden behind it. @LookupIndex makes "I need to query by a second field" an explicit,
generated denormalized table instead of an ORM secretly running ALLOW FILTERING for you.
@PartitionKey/@ClusteringKey make "where does this row physically live, and in what order" a
property of the entity definition, not an implementation detail the ORM promises to handle. This
isn't Kandra being stricter than Hibernate for strictness's sake — it's Kandra refusing to build
the one abstraction (arbitrary-column querying) that doesn't actually exist on this database. See
/why and /foundations for the full argument.
Why not just use the DataStax driver directly?
You can — Kandra is built on top of the DataStax Java driver, not a replacement for it, and
nothing stops you from dropping to CqlSession directly (repo.raw()/repo.rawQuery() exist
precisely for that). What Kandra buys you over hand-written CQL:
- DDL generation and startup validation. Kandra generates
CREATE TABLE/ALTER TABLEfrom your entity's annotations and, depending onSchemaMode, either creates, migrates, or validates the real schema when the plugin installs — not on the first request that happens to hit a mismatched column. Hand-written CQL has no equivalent: a typo in a column name in raw CQL fails at query time, in whatever environment first exercises that code path. - A query DSL that structurally cannot express
ALLOW FILTERING. There's no method onQueryContextthat produces it. Hand-building CQL strings, you can always typeALLOW FILTERING— nothing stops you, and it'll run fine on a small dataset. Kandra's DSL fails at query-build time instead, pointing you at@SecondaryIndex. - Denormalization as annotations, not hand-maintained duplicate write paths.
@LookupIndexgenerates the lookup table's DDL and keeps its writes/deletes in sync with the primary table. With the raw driver, "also writeusers_by_email" is a line of code someone has to remember to add to every write path that touchesusers, forever. See/why#where-did-my-lookup-table-go-out-of-sync. - Batch scope safety.
KandraBatchScope(saveInBatch/deleteInBatch) makes aLOGGED BATCHan explicit, bounded block instead of a raw driverBatchStatementyou assemble by hand and hope every call site remembers to use consistently. - Type-safe codecs and generated
*Tablerefs.kandra-codegen's KSP processor generatesUserTable.email-style column references from your entity, so a renamed field is a compile error in every query that referenced it, instead of a silently-wrong bound CQL string.
The honest tradeoff: all of that is a layer between you and the driver, and every layer is a
place a bug can hide that wouldn't exist in raw CQL — see /battle-scars for a
full accounting of the bugs this project has actually shipped in that layer, and how each one was
found and fixed. Kandra's bet is that structural safety (can't-express-ALLOW FILTERING,
startup-time schema validation, generated lookup tables) is worth the surface area. If your
access patterns are simple enough, or you want zero abstraction between you and the wire protocol,
the driver alone is a completely reasonable choice.
Does this work with plain Apache Cassandra, or only ScyllaDB?
Kandra is built on the DataStax Java Driver (4.17.0), which targets the CQL wire protocol both
Cassandra and ScyllaDB implement — ScyllaDB is wire-compatible with Cassandra's CQL and client
protocol by design, and Kandra doesn't talk to anything below that protocol layer. Nothing in
kandra-core's DDL generation or kandra-runtime's query/codec layer is ScyllaDB-specific CQL —
it's the same CREATE TABLE, INSERT, LWT, and batch syntax either database accepts.
That said, verify the specific claim you're relying on:
- The project's own integration tests (
kandra-test,KandraTestcontainers) run against theorg.testcontainers:cassandraTestcontainers module — an actual Apache Cassandra container — even though the prose throughoutREADME.md/docs/USER_GUIDE.mdconsistently says "ScyllaDB." That's a real signal the driver-level compatibility holds in practice, not just in theory. (Verified againstdocs/USER_GUIDE.md's testing section, current Kandra source as of v0.4.5.) - The project's real-cluster test plan (
docs/test-plan/,docs/report:1.0/) was run exclusively against a 3-node ScyllaDB cluster, not Cassandra. So while nothing in the code is Scylla-specific, the deepest, most adversarial real-cluster verification Kandra has actually had was against ScyllaDB, not Cassandra. - The docs use ScyllaDB-specific language in a few places worth knowing about even though they
don't change behavior — e.g.
batchWarnThresholdKb's default is described as ScyllaDB's batch size recommendation, andgc_grace_seconds/compaction defaults are described in ScyllaDB terms. These are configuration defaults, not hard-coded ScyllaDB dependencies — nothing prevents tuning them for a Cassandra deployment's own defaults.
Bottom line: nothing Kandra-specific is ScyllaDB-only by design or by code path, but the project's own heaviest real-cluster testing has been against ScyllaDB, and that's a meaningfully different level of confidence than "the driver protocol is compatible."
What's the performance overhead?
Verifiable, plainly: Kandra wraps every read and write with a codec layer (Kotlin type ⇄ CQL
type), and every save()/update() path with schema/validation lookups (SchemaRegistry,
optional KandraValidator) before the statement reaches the driver. All suspend read and write
paths use the driver's true async API (executeAsync().await()) rather than blocking a coroutine
dispatcher thread — see ISS-014
— so the concurrency model doesn't add thread-blocking overhead on top of the driver's own.
What's not verifiable, and not measured: there is no published benchmark comparing Kandra's
per-query latency or throughput against the bare DataStax driver. docs/test-plan/ and
docs/report:1.0/ are a functional and correctness test plan against a real 3-node ScyllaDB
cluster — they confirm behavior (does save() do what it says, does a dropped quorum fail fast
rather than hang, ~90ms observed for one quorum-loss write scenario) — not a latency/throughput
benchmark of Kandra's overhead versus raw CQL. If you need a number for capacity planning, this
site doesn't have one to give you honestly; you'd need to benchmark your own access patterns.
How do I test this?
Two genuinely different tools, for two genuinely different things — don't reach for the wrong one:
FakeKandraSession— an in-memory session for fast, structural unit tests (did this code callsaveon the right repository, did the batch scope collect the statements you expected). It does not exercise real LWT semantics, real codec/parameter binding, or real data round-trips — see ISS-020 for exactly where that line is, deliberately.KandraTestcontainers— a real Cassandra/ScyllaDB container per test class, isolated keyspace per class, for tests that need real semantics: real LWT conflicts, real tombstone behavior, real consistency-level effects.
Full walkthrough: /tutorial/12-testing. The reasoning for why both tools
exist and where the line between them sits: /philosophy/testing.
Is Kandra production-ready? Who uses it?
No named production adopters are documented anywhere in the project — this site won't claim
otherwise. What is verifiable: as of the current issue tracker
(docs/issues/README.md), there are zero open issues, and the fixed issues each carry the
real-cluster verification method used (unit test only, vs. confirmed live against a real
3-node ScyllaDB cluster). The 0.4.3/0.4.4/0.4.5 changelog entries are specifically a
find-fix-reverify cycle driven by running a real-cluster test plan (docs/test-plan/,
docs/report:1.0/) against the published Maven Central artifact — and that report is explicit
that its own first run was partial (roughly 43% of its functional coverage matrix), not a
clean bill of health it's hiding a gap in. Read
docs/report:1.0/SUMMARY.md
yourself before deciding what "tested" means for your use case — the honest summary is: rigorously
tested for the bugs it has found and fixed, incompletely tested against the full matrix it set out
to cover, and not yet claimed as production-proven by any named adopter.
What Kotlin/JVM/Ktor versions does this need?
Per gradle.properties and docs/USER_GUIDE.md (current as of v0.4.5):
- Kotlin 2.1.21 (pinned deliberately — Kotlin 2.0.0 has a parser bug against Java 25 in Gradle's embedded compiler)
- Build/run with JDK 21 — the default JDK 25 breaks the Kotlin compiler embedded in Gradle
8.11 for this project;
README.mdcalls this out explicitly (JAVA_HOME=<jdk-21-path>) - Ktor 2.3.13
- DataStax Java Driver 4.17.0
- Gradle 8.11 (per
gradle.properties; the project's own real-cluster test report notes the actual wrapper it ran with was 9.3.0 — a known, low-severity doc/config drift, not a Kandra bug) - KSP 2.1.21-2.0.1 (for
kandra-codegen)
How do I adopt Kandra on an existing Cassandra/ScyllaDB app incrementally?
Nothing in the docs describes a dedicated "adopt Kandra on an existing schema" migration guide — here's what's actually verifiable from the schema-mode and migration primitives that exist:
- Start with
SchemaMode.VALIDATEagainst your existing keyspace. It checks every entity column exists with a compatible type and throwsKandraSchemaExceptionon mismatch, without ever runningCREATE/ALTER— the safest way to point Kandra entities at a schema Kandra didn't create, and find out immediately where your entity definitions and the real table disagree. - Model entities for the tables you actually have first, one at a time — Kandra doesn't require every table in a keyspace to be modeled as a Kandra entity simultaneously; unmapped tables are simply untouched.
kandra-migrate's versioned, checksum-validatedKandraMigration/KandraMigrationRunneris the tool for schema changes going forward once you're on Kandra, not a tool for describing your existing schema retroactively.SchemaMode.NONEdisables all Kandra-managed DDL entirely if you'd rather manage schema externally (existing Liquibase/Flyway-for-CQL tooling, ops-managed DDL) while still getting Kandra's repositories, codecs, and query DSL against that schema.
If your existing schema doesn't cleanly map to Kandra's assumptions (e.g. a table whose delete
semantics don't match @SoftDelete's TTL model, or a key shape Kandra's clustering-key handling
doesn't expect), that's a real gap this FAQ can't paper over — model it by hand against
SchemaMode.VALIDATE/NONE and adopt Kandra's generated-DDL modes only for genuinely new tables.
What if I actually need ALLOW FILTERING, or a query the DSL can't express?
repo.raw()/repo.rawQuery() — an explicit, parameterized escape hatch for exactly this. It
takes a real CQL string (including ALLOW FILTERING, if that's genuinely what you need) with
KandraRawQuery.cql(template).bind(...) for safe positional parameter binding, or the raw()
shorthand for quick cases (it logs a WARN if you call it with string-literal CQL and no bound
parameters — a deliberate nudge against building CQL by interpolation). This is the honest answer
to "Kandra's DSL refuses to build this query": the refusal is in the DSL, not a hard ban on the
underlying CQL feature. You can still reach for it — but only by writing the CQL yourself, in
plain sight in your own code, not by a DSL method that quietly does it for you.
What's still rough — what should make me hesitate?
Reading /battle-scars and docs/issues/ honestly: several of the bugs Kandra has shipped and
fixed (ISS-025 clustering-key WHERE clauses, ISS-026/027 batch scope shadowing, ISS-028/029/030
found during 0.4.4's own re-verification pass) were exactly the kind of "compiles, looks correct,
silently wrong at runtime" failure this library exists to prevent — found in Kandra's own code,
not a user's. That's evidence the correctness bar is taken seriously (every one was caught by
real-cluster testing and fixed with a permanent regression test), but it's also evidence that a
library this young, wrapping a genuinely hard problem (distributed schema + codec + batch
correctness), has had real gaps between "looks right" and "is right." FakeKandraSession's
deliberate real-semantics gap (ISS-020) means a green unit test suite alone doesn't prove your
code is correct against a real cluster — budget for KandraTestcontainers runs, not just fakes,
before you trust a release. See /battle-scars for the full, unflattering
history.