skip to content

Your team debates whether a property may have a custom getter that does I/O or mutates state. What is the contract for getters/setters, and how do you decide between a computed property and a function?

level: seniorimportance: should knowfreq 30%

answer

  1. Getter = cheap, no side effects, doesn't throw
  2. Function when expensive/throws/side-effects/args/new object
  3. Debuggers & logs read properties unexpectedly
  4. Expensive-but-pure => by lazy caching
  5. Setters validate+store, not heavy work

basics

~10 s

Property reads should feel cheap and harmless. Avoid getters that do slow work, throw, or change state. If it's expensive, has side effects, or takes inputs, make it a function instead.

solid answer

~40 s

Idiomatically, a getter must behave like reading a field: cheap, deterministic-ish, no observable side effects, and not throwing for normal inputs — callers assume `obj.x` is harmless and may read it repeatedly (logging, debuggers, equals). A custom getter that performs I/O, blocks, or mutates state violates that contract and surprises readers and tools. Kotlin's coding conventions say: prefer a **function** when the operation is expensive, throws, has side effects, returns a new copy each call, or takes parameters; prefer a **property** for cheap, idempotent derived state. For expensive-but-pure results, use a computed property backed by caching (`by lazy` for one-shot, or a memoized field) rather than recomputing. Setters likewise should only set/validate, not trigger heavy work.

code

kotlin · 8 lines
kotlin
class Report(private val rows: List<Row>) {
    // cheap derived state -> property
    val isEmpty: Boolean get() = rows.isEmpty()
    // expensive but pure -> cached property
    val summary: Summary by lazy { computeSummary(rows) }
    // side-effecting / costly -> explicit function
    fun export(path: Path) { /* writes to disk */ }
}

go deeper

for a junior

Understands a property read should be cheap and not surprising.

for a middle

Can apply the function-vs-property checklist and avoid throwing/side-effecting getters.

for a senior

Explains why tools/equals/logging make hidden side effects dangerous and uses by lazy for expensive-pure values.

for a principal

Sets team conventions, reasons about the cost/idempotence contract as part of API design and its impact on debuggability and concurrency.

## The implicit getter contract When a developer or tool sees `obj.name`, they assume: - It's **cheap** (roughly field-access cost). - It has **no observable side effects** — reading twice equals reading once. - It **doesn't throw** under normal conditions. - It can be read freely by debuggers, `toString()`, logging, and `equals`/`hashCode`. A getter that does network/disk I/O, blocks a thread, or mutates state breaks these assumptions. A debugger evaluating the property in a watch window could fire your I/O; a log line could trigger a side effect. This is a real source of bugs. ## Kotlin coding-convention guidance Prefer a **function** over a property when the work: - is **computationally expensive** (or worse than O(1)); - **throws** exceptions; - has **side effects**; - returns a **different/new object** on each call (so reads aren't idempotent); - requires **arguments**. Prefer a **property** when it's cheap, derived, idempotent, and reads like state (`size`, `isEmpty`, `fullName`). ```kotlin // BAD: hides I/O behind a property read val config: Config get() = loadFromDisk() // surprising, repeated, can throw/block // BETTER: a function makes the cost explicit fun loadConfig(): Config = loadFromDisk() // OR: cheap derived state stays a property val isExpired: Boolean get() = clock.now() > expiry ``` ## Expensive but pure: cache, don't recompute If the value is pure but costly, keep the property syntax and cache: ```kotlin val primes: List<Int> by lazy { sieve(limit) } // computed once, then cached ``` `by lazy` computes on first access and stores the result (thread-safe by default via `LazyThreadSafetyMode.SYNCHRONIZED`). ## Setters A setter should validate and store. Avoid making a setter kick off heavy work, fire network calls, or have surprising cascading effects; route such behavior through an explicit method so the side effect is visible at the call site. ## Decision checklist - Cheap + pure + no args => **computed property**. - Expensive + pure => **property with caching** (`by lazy`) or a function if cost should be explicit. - Side effects / throws / args / new object each call => **function**.

  • Why is a getter that does network I/O dangerous beyond just being slow?
    Tools and code read properties implicitly — debuggers, logging, toString, equals — so the I/O can fire at unexpected times and repeatedly, causing surprising behavior and hard-to-trace bugs.
  • For an expensive-but-deterministic value you still want to expose as a property, what do you use?
    A computed property backed by caching, typically `by lazy { ... }`, which computes once on first read and stores the result thread-safely.

A property read is like glancing at a sign; a function call is like asking someone to go fetch something. Don't hide a fetch behind a glance.

saying these in an interview costs you the question

  • Defending getters that perform blocking I/O or network calls
  • Saying getters may freely throw or mutate state
  • Not knowing the property-vs-function convention
  • Recomputing an expensive value on every read instead of caching
  • Hiding side effects in a setter instead of a named method

context