Kandra

Philosophy

/foundations explains the Cassandra/ScyllaDB mental model. This section explains Kandra's own decisions on top of that model — the places where the library had to pick a default, draw a line, or refuse to do something an ORM built for a relational database would do without asking. Each page here exists because the decision wasn't obvious, and getting it wrong once, in Kandra's own development, is usually why the page exists at all.

Atomicity

LOGGED BATCH is the only cross-table atomicity primitive Cassandra has — there's no ambient transaction to wrap arbitrary calls in. KandraBatchScope exists to make that primitive opt-in and visible, and its methods are named saveInBatch/deleteInBatch instead of save/delete for a specific, load-bearing reason: Kotlin's overload resolution would have made a same-named batch method silently unreachable, not a compile error. This page explains the mechanism and the naming story in full.

Consistency

Every read and write resolves a consistency level from three possible sources — a per-call parameter, a @ReadConsistency/@WriteConsistency annotation on the entity, or the global config default — in a specific, fixed priority order. This page verifies that order against the actual resolution code, not just the KDoc, and explains why Kandra's global read/write defaults are asymmetric, and why Lightweight Transactions aren't the default anywhere.

Schema safety

Kandra runs all DDL and schema validation when the plugin installs — at application startup — not on the first request that happens to hit an unvalidated code path. SchemaMode controls exactly what runs (AUTO_CREATE, AUTO_MIGRATE, VALIDATE, NONE), and none of them do anything at request time. This page walks through what each mode actually executes.

Denormalization

@LookupIndex isn't a workaround for Cassandra's lack of joins — it's the primary tool for a "query-first" schema, and Kandra treats it as a first-class, generated, maintained artifact instead of something you hand-roll and hope stays in sync. This page covers the BATCH vs EVENTUAL consistency choice per lookup field, and what "eventual" actually means mechanically — including what happens when an eventual write fails.

Testing

FakeKandraSession and KandraTestcontainers are not interchangeable, and the fake is deliberately, permanently limited — not a temporary gap. This page explains what the fake can and can't verify, why making it smarter would mean re-implementing Cassandra's data-plane semantics in a test double, and where each tool actually belongs in a test suite.


None of these pages repeat /foundations's explanation of why Cassandra works the way it does — they assume you've read that, and focus on the specific choices Kandra made in response. For the choices in action, see /tutorial; for the bugs these decisions prevented (or, in a few cases, didn't prevent the first time), see /battle-scars.