Skip to content

Kotlin Coding Standards

The Kotlin coding standards for Arda backend services. These are binding, not advisory: they apply when writing or reviewing Kotlin in any Arda repository, and a design document that conflicts with them needs amending rather than implementing.

This page is the entry point. Read the ten rules below first — they cover most of what a review will raise. Follow a link when you need the reasoning or the edge cases; each topic page is self-contained and short enough to read in one sitting.

If you remember nothing else, remember these.

RuleDetailEnforced by
1Anything that can fail returns Result<T>. Not an exception, not a null, not a sentinel.Functions and Result Handling—
2One return per function. Use when, flatMap chains, or local vals to funnel every path to a single exit. Why.Functions and Result HandlingReturnCount
3Never getOrThrow, getOrNull, or !! to pull a value out of a Result or a nullable. Put the logic inside map/flatMap.Functions and Result Handling · Errors and ExceptionsResultUnwrapping · NoNonNullAssertion · SingleBoundaryUnwrap
4Every error is an AppError, using the most specific subclass. Collect all validation failures into AppError.Composite rather than failing on the first.Errors and ExceptionsNonAppErrorFailure · MissingErrorCause
5A bare return type is a claim that the call cannot fail. Earn it structurally — usually with a private constructor — or return Result<T>.Nullability and Return TypesThrowsOutsideResult
6Nullable means “legitimately absent” and nothing else. Deserializing wire input and acting on a value both return Result<T>.Nullability and Return Types—
7If a type has an invariant, make it unconstructable without it — private constructor plus a companion factory returning Result<T>.Construction and Typed ValuesValidatedTypeConstructor
8Never a naked String or UUID for a domain value. Value class when it has rules, type alias when it does not.Construction and Typed ValuesNakedUuidDeclaration
9An entity with a lifecycle goes in a bitemporal Universe, not a hand-rolled table. Ask before deviating.Persistence and Transactions—
10A deferred DB action is a DBIO<T>, returned un-invoked, so the caller controls the transaction. Never collapse it to a value.Persistence and TransactionsCollapsedDBIO

Seven of the ten are checked mechanically rather than by review — see Automated checks below.

Roughly two hours, in this order. Each page assumes the ones before it.

  1. Functions and Result Handling — how every method in the codebase is shaped, and the combinators for composing them. Read this one properly; it is the grammar everything else is written in.
  2. Nullability and Return Types — decide what a method returns before you write its body.
  3. Errors and Exceptions — what travels in the failure channel, and the one place throwing is still correct.
  4. Construction and Typed Values — how invariants are made structural rather than documented.
  5. Persistence and Transactions — the storage decision and the transaction contract.
  6. Service-Layer Architecture — how the layers of a module divide responsibility.
  7. Resources and Dependency Injection — read when you first hold something closeable.

Then read outward. These conventions govern the code; several architecture pages govern the shape it goes into, and you need those before your first module lands — see Related architecture patterns. If this is your first Arda backend change, start from Backend Onboarding instead, which sequences both.

PageCovers
Functions and Result HandlingSingle exit, Result<T> for fallible operations, fail-fast ordering, tail-recursive retry loops, file size and cohesion, one normalizeFailure() per chain, guard-don’t-checkNotNull, .unitify(), the ResultExt combinators
Nullability and Return TypesBare vs. Result<T>; nullable vs. non-null; retrieval vs. deserialization vs. acting on a value; Result<T?> and when it is warranted
Errors and ExceptionsThe AppError hierarchy and choosing a subclass, collecting validations, preserving cause, bootstrap-time validators, non-null assertions
Construction and Typed ValuesSmart constructors, invariants on data classes, URI over URL, domain-typed values, the type-migration checklist
Persistence and TransactionsDatabase mappings, Universe vs. plain Exposed table, DBIO and the transaction requirement, work that must not join the caller’s transaction
Service-Layer ArchitectureThe Universe boundary, sagas and orchestrators, wire surfaces carrying EntityRecord, shared vocabulary
Resources and Dependency Injectionuse and what to do when it does not fit, the surrogate-close anti-pattern, constructor injection
Automated ChecksAdopting the standards plugin, declaring what the repository is, reading a report, choosing a scope, and writing rules of your own

Short enough to live here.

Enforced by LogProviderEnclosingClass (tree).

  • When a function has multiple parameters of the same type, use named arguments at the call site to prevent argument-order mistakes.
  • When implementing LogEnabled by LogProvider(...), always pass the enclosing class as the argument: LogProvider(MyUniverse::class). Never copy a LogProvider(...) delegation from a neighboring file without updating the class reference.
  1. Do not reformat existing code unless you have been explicitly asked to do so. Leave formatting to the project’s automated tools.
  2. For new code, follow the style defined in the .editorconfig file at the repository root.

Enforced by WildcardImport and UnusedImport (both tree).

  1. Prefer explicit imports over wildcard imports.
  2. Remove all unused imports before finishing a task.

settings.gradle.kts files containing includeBuild("../common-module") or similar local composite build overrides must never be committed or pushed. Always exclude or stash settings.gradle.kts before staging commits.

Refer to the unit-tests skill in the workspace for detailed unit testing guidance, and to Backend Testing.

Most of what is written here is enforced by the Kotlin Standards plugin rather than left to review — twenty-five rules across two engines, each reporting against the section of this document that makes its claim. The Enforced by column above names the rule for each headline claim; the topic pages carry the same marker beside the individual sections.

Three of the ten headline rules are not mechanized, and that is a statement about them rather than a gap in the tooling. Rules 6 and 9 turn on intent a checker cannot read — whether an absence is legitimate, whether an entity has a lifecycle — and rule 1 is the general principle whose checkable shadow is rule 5.

Automated Checks is the page for adopting the gate in a repository, reading its report, changing the scope a rule is held at, and adding rules of your own. Two things worth knowing before you meet a finding:

  • Every rule appears in every report, in exactly one of three states — findings, CLEAN (ran, found nothing), or SKIPPED (did not run, and why). A rule that ran and found nothing must never look like a rule that never ran.
  • A departure is stated where it applies. @Suppress with the reason above it puts the decision in the diff, where a reviewer meets it — and a waiver that is no longer holding anything back fails the build rather than becoming sediment.

These conventions are shaped by the platform, and occasionally the platform is what is wrong. If a rule makes a design impossible rather than merely inconvenient, say so — in the design document’s departures list, or in review. Rules have been changed for good arguments before. A departure that is named is a decision; one that is silent is a defect.

The conventions on this page govern how code is written. These govern what it is written into:


Copyright: (c) Arda Systems 2025-2026, All rights reserved