skip to content

How should the read-only vs mutable interface split inform the types you choose for function parameters, return values, and class properties?

level: seniorimportance: should knowfreq 45%

answer

  1. Least privilege: read-only by default
  2. Param List accepts MutableList too
  3. Return List; copy via toList() to stop aliasing
  4. Backing-property: public val + private _mutable
  5. buildList gives scoped mutable, returns read-only

basics

~20 s

Use the read-only type (List, Set, Map) almost everywhere — for inputs you only read and for things you return. Only use the Mutable type when the function or caller really needs to change the collection.

solid answer

~40 s

Default to the **read-only** interface to follow the principle of least privilege. For **parameters**, take `List<T>`/`Set<T>`/`Map<K,V>` unless the function's contract is to mutate the caller's collection — read-only also accepts a wider range of arguments since `MutableList` is a `List`. For **return types**, return `List<T>` so callers can't accidentally mutate internals; if the value comes from a mutable backing field, return it as `List` and ideally a copy (`toList()`) to prevent aliasing leaks. For **properties**, expose `val items: List<T>` publicly while keeping a `private val _items = mutableListOf<T>()` backing — the classic backing-property pattern. Prefer `val` over `var` and read-only over mutable to make code easier to reason about. Reserve `MutableList` parameters for genuine builder/accumulator scenarios, and document that the function will modify the argument.

code

kotlin · 7 lines
kotlin
class Inventory {
    private val _items = mutableListOf<String>()
    val items: List<String> get() = _items          // expose read-only
    fun add(item: String) { _items += item }         // mutation behind a method
}

fun report(items: List<String>) = items.joinToString()  // read-only param

go deeper

for a junior

Can pick List for reading and MutableList for changing in simple cases.

for a middle

Applies least-privilege defaults and knows the backing-property pattern.

for a senior

Reasons about aliasing leaks, defensive copies, and when buildList/immutable collections are warranted.

for a principal

Sets team conventions for collection types in public APIs and balances encapsulation, performance, and interop.

## Principle: least privilege via types The split is an **API-design lever**. Each position (parameter, return, property) should request the *minimum* capability it needs. ## Parameters Prefer the read-only type: ```kotlin fun average(xs: List<Int>): Double = xs.sum().toDouble() / xs.size ``` Benefits: (1) the signature promises no mutation; (2) it accepts **both** `List` and `MutableList` arguments because `MutableList` is a subtype. Take `MutableList<T>` *only* when mutating the caller's collection is the explicit job (e.g. `fun fillDefaults(target: MutableList<T>)`), and say so in the doc. ## Return types Return `List<T>` to keep callers from mutating your internals: ```kotlin class Cart { private val _lines = mutableListOf<Line>() val lines: List<Line> get() = _lines // read-only view fun add(line: Line) { _lines += line } } ``` Note the view aliases `_lines`, so a downcast could still mutate it. If callers are untrusted or you need a true snapshot, return `_lines.toList()` (defensive copy) or an immutable type from `kotlinx.collections.immutable`. ## Properties: the backing-property pattern Expose a read-only `val` and hide a mutable backing field with a leading underscore (`_lines`). This is idiomatic Kotlin and keeps mutation centralized behind methods, preserving invariants. ## var vs val and read-only vs mutable Two orthogonal axes: - `val`/`var` controls whether the **reference** can be reassigned. - `List`/`MutableList` controls whether the **contents** can be changed through that reference. Idiomatic default: `val xs: List<T>` (neither reassignable nor mutable through this handle). Loosen one axis at a time, only when needed. ## When mutable is right - Local accumulators inside a function (`val acc = mutableListOf<T>()`) — never exposed. - Performance-sensitive builders; consider `buildList { ... }` which gives a scoped `MutableList` and returns a read-only `List`. - Explicit accumulator parameters in internal helpers. ## Summary table - Input you only read → `List`. - Input you must mutate → `MutableList` (documented). - Return value → `List` (copy if leaking internals matters). - Public property over mutable state → read-only `val` + private `_backing` mutable field.

  • Why prefer List over MutableList for a parameter you only read?
    It documents 'no mutation', prevents accidental writes, and still accepts MutableList arguments via subtyping.
  • The backing-property pattern returns a List view of a mutable field — what residual risk remains?
    Aliasing: a caller can downcast the view to MutableList. Return toList() or an immutable type if that matters.

saying these in an interview costs you the question

  • Defaulting every parameter to MutableList 'just in case'
  • Exposing a public mutable property and letting callers mutate internal state
  • Confusing val/var with read-only/mutable
  • Returning the live mutable backing field without considering aliasing
  • Never reaching for toList() or immutable collections when encapsulation matters

context