Part 4 · 1 chapters · ~8 min

Idioms

Immutability and copy(), read-only versus mutable collections, scope functions and their overuse, require, check and error, sealed result types versus exceptions, runCatching pitfalls with coroutines (catching CancellationException), avoiding !!, coroutine scope ownership, naming, and ktlint and detekt rules.

5

Domain errors as types

code
sealed interface TransferResult {
    data class Ok(val transfer: Transfer) : TransferResult
    data class InsufficientFunds(val available: Kobo) : TransferResult
    data object LimitExceeded : TransferResult
}

suspend fun createTransfer(cmd: CreateTransfer): TransferResult {
    require(cmd.amount.value > 0) { "amount must be positive" }        // a bug if violated
    val account = accounts.lock(cmd.from)
    if (account.available < cmd.amount) return TransferResult.InsufficientFunds(account.available)
    return TransferResult.Ok(ledger.post(cmd))
}

// pitfall: runCatching also catches CancellationException, breaking structured concurrency
runCatching { fetch() }.onFailure { if (it is CancellationException) throw it }
IDIOMATIC KOTLIN
what reviewers look for
immutabilityval, read-only collections, dataclass copy().scope functionslet, apply, also, run, with: usedsparingly.Result and sealed errorsrunCatching, or sealed resulttypes for domain errors.require and checkPreconditions that throw clearexceptions.no !!Handle nulls explicitly; !! onlywith a reason.coroutine scopesNo GlobalScope; tie work to alifecycle.
swipe the figure sideways, or tap expand for full screen
1/4
immutable by default
Prefer val and read-only collections; update with copy(). Mutable state should be local or carefully owned.
val and copy()local mutation only