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 eleven 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.
Start here — the eleven rules
Section titled “Start here — the eleven rules”If you remember nothing else, remember these.
| Rule | Detail | Enforced by | |
|---|---|---|---|
| 1 | Anything that can fail returns Result<T>. Not an exception, not a null, not a sentinel. | Functions and Result Handling | — |
| 2 | One return per function. Use when, flatMap chains, or local vals to funnel every path to a single exit. Why. | Functions and Result Handling | ReturnCount |
| 3 | Never 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 Exceptions | ResultUnwrapping · NoNonNullAssertion · SingleBoundaryUnwrap |
| 4 | Every error is an AppError, using the most specific subclass. Collect all validation failures into AppError.Composite rather than failing on the first. | Errors and Exceptions | NonAppErrorFailure · MissingErrorCause |
| 5 | A 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 Types | ThrowsOutsideResult |
| 6 | Nullable means “legitimately absent” and nothing else. Deserializing wire input and acting on a value both return Result<T>. | Nullability and Return Types | — |
| 7 | If a type has an invariant, make it unconstructable without it — private constructor plus a companion factory returning Result<T>. | Construction and Typed Values | ValidatedTypeConstructor |
| 8 | Never a naked String or UUID for a domain value. Value class when it has rules, type alias when it does not. | Construction and Typed Values | NakedUuidDeclaration |
| 9 | An entity with a lifecycle goes in a bitemporal Universe, not a hand-rolled table. Ask before deviating. | Persistence and Transactions | — |
| 10 | A deferred DB action is a DBIO<T>, returned un-invoked, so the caller controls the transaction. Never collapse it to a value. | Persistence and Transactions | CollapsedDBIO |
| 11 | An impossible state gets no design of its own. A state the service’s own writers cannot produce goes to Sentry as an error. It fails the request (500) when the failure stays inside that request. When failing would reach beyond the request, it is logged at ERROR and read as the nearest safe value. | Errors and Exceptions | — |
Seven of the eleven are checked mechanically rather than by review — see Automated checks below.
Reading order for your first module
Section titled “Reading order for your first module”Roughly two hours, in this order. Each page assumes the ones before it.
- 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.
- Nullability and Return Types — decide what a method returns before you write its body.
- Errors and Exceptions — what travels in the failure channel, and the one place throwing is still correct.
- Construction and Typed Values — how invariants are made structural rather than documented.
- Persistence and Transactions — the storage decision and the transaction contract.
- Service-Layer Architecture — how the layers of a module divide responsibility.
- 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.
The topic pages
Section titled “The topic pages”Load a topic page when its case is in the code in front of you; a review loads the pages whose cases are in the diff. A case list names the common cues, not every one: when the code does something a page’s “Covers” column names, load that page too.
| Page | Covers | Load it when the code has |
|---|---|---|
| Functions and Result Handling | Single exit, Result<T> for fallible operations, when over if and direct conditions, KDoc @param and @return, fail-fast ordering, tail-recursive retry loops, file size and cohesion, package cohesion, one normalizeFailure() per chain, guard-don’t-checkNotNull, .unitify(), the ResultExt combinators | a function that can fail; a Result chain (map, flatMap, normalizeFailure()); getOrThrow or getOrNull; more than one return; an if or when; a KDoc comment; a new or moved data class; a retry loop; a file or a package that keeps growing (a package over 25 main files) |
| Nullability and Return Types | Bare vs. Result<T>; nullable vs. non-null; retrieval vs. deserialization vs. acting on a value; Result<T?> and when it is warranted | a choice of return type (bare, Result<T>, nullable, Result<T?>); code that reads wire input |
| Errors and Exceptions | The AppError hierarchy and choosing a subclass, collecting validations, preserving cause, impossible states, bootstrap-time validators, non-null assertions | a try, catch or throw; an AppError; a failure built or converted (Result.failure(...), an exception mapped to an error); a failure recovered to a value (getOrElse, recover); a state the service’s own writers cannot produce; a !!; a validation; a cause; a validator run at startup |
| Construction and Typed Values | Smart constructors, invariants on data classes, URI over URL, domain-typed values, values on a scale (categorical, ordinal, interval), the type-migration checklist | a constructor, data class or value class with an invariant; a domain value held in a base type (String, UUID, Int, Long or another); a value with an order or a distance (a sequence number, a rank, a time); a URL; a type migration |
| Persistence and Transactions | Database mappings, Universe vs. plain Exposed table, DBIO and the transaction requirement, every database access a boundary or a participant, work that must not join the caller’s transaction | an Exposed table or mapping; a Universe; a DBIO; a transaction; a method that reads or writes the database (is it a boundary or a participant?) |
| Service-Layer Architecture | The Universe boundary and every read of a Universe’s rows a member of that Universe, service composition (interfaces as contracts, one class per concern), sagas and orchestrators, wire surfaces carrying EntityRecord, shared vocabulary | a service, saga or orchestrator; an interface with method bodies; a read of a Universe’s rows outside it (exec, a Filter on column names); a wire or read surface (a DTO, a response, an EntityRecord); a reference to another entity on the wire; a shared constant; a call across a Universe boundary |
| Resources and Dependency Injection | use and what to do when it does not fit, the surrogate-close anti-pattern, constructor injection | something closeable; use; a dependency, injected or built in place (a service, an SDK client) |
| Automated Checks | Adopting the standards plugin, declaring what the repository is, reading a report, choosing a scope, and writing rules of your own | the standards plugin’s setup or version; an ardaStandards { ... } scope (ratchet, advisory); a standards-plugin or detekt finding; a @Suppress; a repository’s standards declaration; a new rule |
Universal rules
Section titled “Universal rules”Short enough to live here.
General
Section titled “General”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 aLogProvider(...)delegation from a neighboring file without updating the class reference.
Formatting
Section titled “Formatting”- Do not reformat existing code unless you have been explicitly asked to do so. Leave formatting to the project’s automated tools.
- For new code, follow the style defined in the
.editorconfigfile at the repository root.
Import Statements
Section titled “Import Statements”Enforced by WildcardImport and UnusedImport (both tree).
- Prefer explicit imports over wildcard imports.
- Remove all unused imports before finishing a task.
Composite Build Safety
Section titled “Composite Build Safety”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.
Unit Tests
Section titled “Unit Tests”Refer to the unit-tests skill in the
workspace for detailed unit
testing guidance, and to Backend Testing.
Automated checks
Section titled “Automated checks”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.
Four of the eleven 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. Rule 1 is the general principle whose checkable shadow is rule 5. Rule 11 turns on what every writer of a value can produce, which a checker cannot see from the read site; the gate catches only its dropped form (ResultUnwrapping, FoldToNull).
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), orSKIPPED(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.
@Suppresswith 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.
When a rule pushes back on your design
Section titled “When a rule pushes back on your design”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.
Related architecture patterns
Section titled “Related architecture patterns”The conventions on this page govern how code is written. These govern what it is written into:
- Data Authority Module Pattern — the four-layer module shape.
- DAG Package Discipline — which package each type belongs in, and the no-cycles rule at every aggregation level.
- Module Wiring Entry Point — the canonical
Application.<module>(…)entry point. - Endpoint Definition DSL — how a REST surface is declared. Never raw Ktor routing in a module.
- API Design — URL shape, headers, and the error boundary a thrown
AppErrorreaches. - Naming Conventions — what to call packages, types, routes, and tables.
- Universe Design — building a Universe once you have chosen one.
- Functional Programming at Arda — the Railway Oriented Programming rationale behind rules 1–3.
- Exception Handling — the
AppErrorhierarchy in architectural terms.
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved