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.

RuleDetail
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.Functions and Result Handling
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 Exceptions
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 Exceptions
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 Types
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 itprivate constructor plus a companion factory returning Result<T>.Construction and Typed Values
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 Values
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 Transactions

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
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

Short enough to live here.

  • 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.
  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.

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.

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:


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