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.
Start here — the ten rules
Section titled “Start here — the ten 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 |
Seven of the ten 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”| Page | Covers |
|---|---|
| Functions and Result Handling | Single 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 Types | Bare vs. Result<T>; nullable vs. non-null; retrieval vs. deserialization vs. acting on a value; Result<T?> and when it is warranted |
| Errors and Exceptions | The AppError hierarchy and choosing a subclass, collecting validations, preserving cause, bootstrap-time validators, non-null assertions |
| Construction and Typed Values | Smart constructors, invariants on data classes, URI over URL, domain-typed values, the type-migration checklist |
| Persistence and Transactions | Database mappings, Universe vs. plain Exposed table, DBIO and the transaction requirement, work that must not join the caller’s transaction |
| Service-Layer Architecture | The Universe boundary, sagas and orchestrators, wire surfaces carrying EntityRecord, shared vocabulary |
| Resources and Dependency Injection | use and what to do when it does not fit, the surrogate-close anti-pattern, constructor injection |
| Automated Checks | Adopting the standards plugin, declaring what the repository is, reading a report, choosing a scope, and writing rules of your own |
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.
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), 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