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 | |
|---|---|---|
| 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. | Functions and Result Handling |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
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 |
| 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 |
Universal rules
Section titled “Universal rules”Short enough to live here.
General
Section titled “General”- 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”- 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.
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 {#related-architecture-patterns}
Section titled “Related architecture patterns {#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