Kotlin Coding Standards
This page documents the Kotlin coding standards for Arda backend services. Apply these rules when writing or reviewing Kotlin code in any Arda repository.
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.
Functions and Methods
Section titled “Functions and Methods”-
Any function or method that may fail must return
Result<T>instead of throwing an exception. -
Single exit point. Functions and methods must have a single
returnstatement (or a single expression body). Do not scatter multiplereturnorreturn@labelstatements throughout a function body. Instead, usewhenexpressions,Result.flatMapchains, or localvalbindings to funnel all paths to one exit. Multiple early returns make control flow hard to follow and easy to break during refactoring.// WRONG — multiple return pointssuspend fun validate(url: String): Result<URL> {val parsed = try { URL(url) } catch (e: Exception) {return Result.failure(AppError.ArgumentValidation("url", e.message))}if (parsed.host != expectedHost) {return Result.failure(AppError.ArgumentValidation("url", "wrong host"))}return Result.success(parsed)}// CORRECT — single expression using flatMap chainsuspend fun validate(url: String): Result<URL> =runCatching { URL(url) }.mapError { AppError.ArgumentValidation("url", it.message) }.flatMap { parsed ->when {parsed.host != expectedHost -> Result.failure(AppError.ArgumentValidation("url", "wrong host"))else -> Result.success(parsed)}} -
Prefer
whenexpressions overifstatements wherever possible. -
Use
Result.map,Result.flatMap, and similar operators to chain operations that may fail. Model the logic around Success and Failure channels. -
Do not use
getOrThroworgetOrNullto extract a value from aResult. UsemaporflatMapand place the logic inside the lambda. -
Result<T>for all fallible operations. Any operation that can fail — including URL construction, parsing, or other seemingly simple operations — must returnResult<T>. Consistency matters: if there is any code path that throws, wrap it inrunCatchingand returnResult. -
Fail-fast ordering in chains. When chaining multiple operations, order them cheapest-first. Validate inputs (pure logic, no I/O) before accessing
ApplicationContext(coroutine context) or making service calls (network). -
flatInApplicationContextfor coroutine context access. UseApplicationContext.Key.flatInApplicationContext { ctx -> ... }to accessApplicationContextfrom a coroutine. Do not useApplicationContext.current().flatMap { ... }—flatInApplicationContextis the idiomatic common-module pattern. -
Data classes before their producers. Define data classes (result types, value objects) above the class that produces or consumes them. The reader encounters the type before the code that uses it.
Result Handling
Section titled “Result Handling”In production code, never unwrap a Result with getOrThrow or getOrNull.
Always use map or flatMap to work with the value while keeping it in the
Result context. If this cannot be done reasonably for a given case, escalate
to the team for guidance.
Exception and Error Handling
Section titled “Exception and Error Handling”- All exceptions must be a subclass of
cards.arda.common.lib.lang.errors.AppError, using the most specific subclass available. - Choose the exception subclass by origin:
AppError.Invocation— errors attributable to inputs to the method.AppError.ArgumentValidation(argumentName, validationMessage)— invalid input parameter.AppError.ContextValidation(msg)— invalid request context (e.g., missing tenant scope).
AppError.Internal— errors arising from internal logic or implementation-resource issues.AppError.ExternalService(msg, code, description)— failure in an external service call (e.g., AWS SDK error).AppError.IncompatibleState(message)— internal state inconsistency.AppError.Infrastructure(message)— infrastructure/configuration error.AppError.NotFound(resourceName)— requested resource does not exist.
AppError.Composite(message, causes)— wraps multiple errors together, as in input validation, collection operations, or batch operations. UseThrowable.normalizeToAppError()to convert generic exceptions toAppError.
- Handle errors by returning a
kotlin.Resultwherever possible. - When integrating external libraries or calling methods that cannot return
kotlin.Result(e.g., constructors,toString,equals):- Run them inside a
runCatchingblock. - Convert the exception to an
AppErrorusingmapError, either manually or via the provided extension functionsThrowable.normalizeToAppError()orResult<T>.normalizeFailure().
- Run them inside a
- Collect all errors in validation functions. When a function validates
multiple conditions, collect all failures into a list and return
AppError.Compositewhen there are multiple errors, a singleAppErrorwhen there is one, orResult.successwhen there are none. Do not fail-fast on the first error — the caller (and ultimately the user) needs the complete picture to fix all issues at once.
Non-Null Assertions (!!)
Section titled “Non-Null Assertions (!!)”Never use !! (the non-null assertion operator). It bypasses the compiler’s
null-safety guarantees and throws KotlinNullPointerException at runtime —
exactly the class of error that Kotlin’s type system is designed to prevent.
Instead, use one of the following patterns that give the compiler enough information to smart-cast the value:
// WRONG — runtime crash if nullval name = user.name!!
// CORRECT — when expression with smart castval name = when (val n = user.name) { null -> return Result.failure(AppError.Infrastructure("name is required")) else -> n // compiler knows n is non-null here}
// CORRECT — if/else with smart castval name = user.name ?: return Result.failure(AppError.Infrastructure("name is required"))
// CORRECT — require() for preconditionsval name = requireNotNull(user.name) { "name must not be null" }The when or if pattern is preferred because it keeps failure handling in the
Result channel. Use requireNotNull only for true programming errors where a
null value indicates a bug, not a user or configuration error.
Do not use nullable types to represent missing capabilities. If a class parameter is only needed by some callers, do not make it nullable to “disable” a feature. Instead, construct the class with full capabilities and let callers use only the methods they need. For example, a service that supports both PUT and POST should always be constructed with both capabilities — a caller that only needs PUT simply does not call the POST method.
Dependency Injection
Section titled “Dependency Injection”- Inject dependencies, do not construct them internally. Classes should receive their dependencies (services, clients, configuration) as constructor parameters. The wiring point (Module.kt) creates all dependencies and passes them in. Classes should not create their own service instances, SDK clients, or other infrastructure objects.
- When a class needs access to an internal component of a dependency (e.g., an
S3AsyncClientowned by anS3AssetService), the dependency should expose it as a public property rather than having the consumer construct a separate instance.
Null Value Semantics
Section titled “Null Value Semantics”-
Null should only be returned from methods that explicitly search or retrieve information. In these cases the method returns
Result<T?>:- A
nullpayload means the value was not found. - A
failuremeans a different condition occurred (e.g., a required parent record was itself missing).
Example: retrieving a child record from a parent-child relationship — if the parent is null, return
failure. If the parent is found but the child is not, returnsuccesswith a null payload. - A
-
In all other cases (e.g., mutating system state), if the target of the operation is null or not found, return
Result.failure(...).
Database Mappings
Section titled “Database Mappings”Arda uses Exposed for database access.
-
Column names must be specified in lowercase snake_case.
-
Use column types defined under
EntityTable. When those types are insufficient, use native Exposed column types. -
When using
Filter.Eqand similar, use<TBL>.<COLUMN>.namefor the locator parameter instead of hardcoded strings, to eliminate the risk of mismatched column names. -
JSON columns with custom serializers: The reified
json<T>(name, format)overload callsserializer<T>()at runtime, which ignores@Serializable(with=...)and@Contextualannotations on type arguments. WhenTcontains a non-@Serializableclass (e.g.,java.net.URI), use the three-argument overload with an explicitKSerializer:// WRONG — fails at runtime: serializer<Map<String, URI>>() cannot find URI serializerval sites = tbl.json<Map<String, URI>>(name, JsonConfig.standardJson)// CORRECT — explicit serializerval sites = tbl.json<Map<String, URI>>(name, JsonConfig.standardJson, MapSerializer(String.serializer(), URISerializer))EntityTablealso providesstandardJson<T>(name), which resolves contextual serializers fromJsonConfig.standardJson.serializersModule. Prefer it overjson<T>(name, JsonConfig.standardJson)when contextual serializers are sufficient. -
Unchecked casts to
ChildTable: When castingEntityTabletoChildTable(required due to invariant generics onExposedLocatorTranslator), always guard withcheck()before the cast:override val translator by lazy {check(persistence.bt is ChildTable) {"MyUniverse requires a ChildTable, got ${persistence.bt::class}"}queryConfig.bindToTable(persistence.bt as ChildTable)} -
QueryCompilerreuse: Never constructQueryCompiler(table)inline inside service methods. Define a module-levellazy valin the persistence package, or expose aninternalaccessor on the universe:// Module-level lazy val (preferred for child universes)internal val myQCompiler by lazy {QueryCompiler(MY_TABLE, myQueryConfig.bindToTable(MY_TABLE))}// Internal accessor on parent universeinternal val queryCompiler get() = qCompiler
URL and URI Types
Section titled “URL and URI Types”- Prefer
java.net.URIoverjava.net.URLfor URL-typed fields.URL.equals()andURL.hashCode()trigger DNS resolution, makingURLunsafe as a map key, in collections, or inside data classes. - Use
URI(...).toURL()instead ofURL(String). TheURL(String)constructor is deprecated in modern Java. Construct viaURIfirst, then convert:URI("https://example.com/path").toURL(). - Both
URISerializerandURLSerializerare registered as contextual serializers inJsonConfig.standardJson. UseURISerializerexplicitly when needed (see Database Mappings rule 4 above). - The wire format for
URIis a plain JSON string. Changing a field fromStringtoURIrequires no database migration.
Type Migration Checklist
Section titled “Type Migration Checklist”When changing a field type in a domain value object (e.g., String to URI),
update all three layers:
- Domain: sealed interface property and
Valuedata class property, including any default values. - Persistence: component column definition,
fill(),getComponentValue(), andsetComponentValue(). - Tests: update test data construction and assertions.
Verify that the wire format (JSON serialization) is unchanged if no database migration is planned.
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.
Import Statements
Section titled “Import Statements”- Prefer explicit imports over wildcard imports.
- Remove all unused imports before finishing a task.
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved