Binding style rules for Kotlin projects, target Kotlin 2.3. Prioritize correctness, explicitness, simplicity — never cleverness, never abstraction for its own sake.
This guide is platform-agnostic. It covers the Kotlin language, the standard library, and kotlinx ecosystem. JVM-specific rules (Java interop, Spring/JPA, Loom, Jackson, SLF4J) live in the companion Kotlin-JVM guide.
This guide extends and defers to the Kotlin official coding conventions and the Google Android Kotlin style guide. Where our guidance conflicts with theirs, the official conventions win. This guide adds project-specific conventions on top: assertion density (TigerBeetle-inspired), 60-line function limit, explicit bounds on every loop/queue/retry, and Result<T, E>-as-sealed-ADT discipline.
- Clarity — code's purpose is clear to the reader.
- Simplicity — the simplest approach that accomplishes the goal.
- Concision — high signal-to-noise ratio.
- Maintainability — easy to modify correctly over time.
- Consistency — matches the surrounding codebase.
Resolve rule conflicts in this order. Consistency is the tiebreaker, never an override.
| # | Document | Scope |
|---|---|---|
| 01 | Formatting & Tooling | ktlint, detekt, EditorConfig, line length, trailing commas, expression bodies, function size cap |
| 02 | Naming Conventions | Scope-proportional names, camelCase/PascalCase, packages, backticks, properties vs getters/setters |
| 03 | Nullability | Non-null default, !! banned, Elvis, smart casts, requireNotNull, contracts |
| 04 | Variables & Declarations | val vs var, immutable collections, lateinit vs lazy, top-level vs object, delegation |
| 05 | Functions | Expression vs block bodies, default + named args, inline/crossinline/noinline, extensions |
| 06 | Classes & Data Modeling | data class, value class, sealed hierarchies, object, composition over inheritance |
| 07 | Kotlin Idioms: Sugar with Intent | by delegation, scope functions, builders, expression-oriented forms, stdlib contracts, operators/infix, type aliases |
| 08 | Error Handling | Sealed Result<T, E>, exceptions for unrecoverable, exhaustive when, no swallowing |
| 09 | Concurrency & Async | Coroutines, structured concurrency, dispatchers, Flow, Mutex, Channel |
| 10 | API Design | Interfaces, generics + variance, default args vs builder, visibility, @RequiresOptIn, pipeline pattern |
| 11 | Testing | Parameterized tests, no shared state, fixtures, property-based, assertion discipline |
| 12 | Module Organization | Source-set / package layout, internal, public surface, no cyclic deps |
| 13 | Resource Management | use {}, AutoCloseable, structured cancellation, withTimeout, secure random |
| 14 | Documentation | KDoc on public API, @param/@return/@throws/@sample, package docs, why over what |
| 15 | Performance | inline, value classes, Sequence vs List, allocations, no premature opt |
Security, performance, and git practices are covered in the root-level code style guide. JVM-specific concerns are in the Kotlin-JVM guide.
Correctness > performance > developer experience. When they conflict, this ordering decides.
Kotlin gives you both worlds: data classes, sealed hierarchies, immutable collections, and structured concurrency on one side; classes, inheritance, and unchecked exceptions on the other. We pick the safe side every time and use the rest only where it pays.
- Data + functions, not objects.
data classfor state. Top-level / extension functions for transformations. Sealed hierarchies for closed polymorphism. Composition +bydelegation for code sharing — never open inheritance. - Nullability is part of the type — never
!!. Resolve at the boundary using smart casts,?:,?., orrequireNotNull.!!is a missing model, not a shortcut. valovervar. Immutable collections over mutable. Mutability is an explicit choice you have to type — that's the right way around.- Explicit over implicit. No magic. Every dependency in the constructor or function signature. No reflection-driven control flow in new code. No global mutable state.
- Errors are values. Sealed
Result<T, E>for expected/domain failures. Exceptions only for unrecoverable conditions and programmer errors. Wrap at the boundary; do not let exceptions cross module boundaries silently. - Pick the concurrency primitive deliberately. Coroutines for most async/cancellation-aware code. Other primitives only when the system forces it. Name the seam.
- Small functions, breathing room. Hard limit: 60 lines. Aim for 15–30. Separate logical sections with blank lines.
- Assert aggressively.
requirefor caller contracts,checkfor state invariants,error(...)for unreachable branches. Minimum two assertions per function on average. Pair-assert when feasible. - Bound everything. All loops, retries, queues, timeouts, coroutine scopes, and
Flowoperators with potentially-unbounded sources must have a fixed upper bound. No unbounded recursion in library code — usetailrec(which the compiler rewrites to a loop) when recursion is the natural shape of the problem; otherwise iterate. - Exhaustive
whenover sealed hierarchies. Noelsefor closed sets — let the compiler tell you when a new variant breaks the world. - Compose via delegation (
by), not inheritance. Class delegation for decoration. Property delegation (by lazy,Delegates.observable, custom) for backing-field discipline. - Scope functions have intent — pick by purpose.
apply(configure, return receiver),also(side-effect, return receiver),let(transform / null-resolve, return result),run/with(group, return result). Never use one because it's familiar; choose by what should be returned. - Embrace expression-oriented Kotlin.
when/if/tryas expressions. Single-expression functions for one-liners. String templates over concatenation. Type-safe builders over builder classes. Let the language replace boilerplate. - Performance from the outset, but pay for what you use.
inlinefor higher-order hot paths.value classfor ID/wrapper types.Sequencefor chained transforms on large collections. Don'tinlineeverything; don'tSequence-ify short lists. - Zero technical debt. What exists meets the design goals. Public API hardens fast —
internalaggressively,@RequiresOptInfor experimental.
This guide takes the Kotlin official coding conventions as canonical. The first entry below is a genuine deviation — a place the conventions take a position we override (the 100-column line limit); the second is an addition the conventions do not address (the 60-line function cap). Each is recorded so it can be revisited surgically. The Google Android Kotlin style guide is supplemental, not a separate baseline; where it is stricter than the conventions it informs a rule, but it adds no deviation of its own.
| Rule | Upstream position | Our position | Why |
|---|---|---|---|
| Line length | Kotlin conventions / ktlint default: 100 columns | 120-column hard limit | Kotlin signatures — named args, generic bounds, lambda types — genuinely run wider than Java, and 120 still fits side-by-side diffs at modern resolutions. See 01-formatting-and-tooling.md. |
| Function size | No upstream cap | 60-line hard cap, ktlint/detekt-enforced; aim 15–30 | Owner decision; Tiger Style discipline, the Kotlin-scaled sibling of Go's 70. See 05-functions.md. |
- Kotlin official coding conventions — canonical authority. Where our guidance collides, the official conventions win (save the deviations above).
- Google Android Kotlin style guide — supplemental; useful for several rules even outside Android.
- Effective Kotlin (Marcin Moskała) — the closest thing Kotlin has to a community canon.
- TigerBeetle Tiger Style — assertion density, 60-line function limit, limits on everything, no recursion, zero technical debt.
When adopting a new rule or migrating away from a deprecated pattern, apply the change at the module / package level or larger — never mix two styles within the same package. A half-migrated package is more confusing than either end state.
Perfection over technical debt — debt never gets paid