When is apply the wrong tool for a builder, and what does configuring a mutable object in place imply for immutability, nullability, and concurrency?
answer
- apply needs var/setters; returns mutable object
- No required-field check; defaults silently kept
- No immutability, no defensive copy
- Not thread-safe; build-then-publish instead
- Prefer constructor + named/default args for real builders
basics
~20 sapply mutates a real object in place, so it needs mutable (var) properties and gives you no immutability or thread-safety. If you need validation, required fields, or an immutable result, a constructor, named arguments, or a real builder is usually better.
solid answer
~40 sapply only configures an existing mutable object, so it requires var properties (or setters) and yields a still-mutable object — no defensive copy, no immutability, no enforced invariants. It can't guarantee required fields are set: a field left unconfigured keeps its default (possibly null), so apply offers no compile-time completeness check the way a constructor with required parameters does. It's also not thread-safe: configuring an object via apply and publishing it before the writes are visible can race. Prefer Kotlin's primary constructor with default and named arguments for most 'builder' needs (compile-time required-field enforcement, immutable val result). Use apply for in-place setup of inherently mutable objects (Java config beans, StringBuilder, framework objects). For complex conditional construction producing an immutable result, a dedicated builder with a build() returning an immutable type beats apply.
go deeper
Uses apply to set fields but may not see the immutability/required-field gaps.
Knows apply needs var and prefers constructors with named args for required fields.
Articulates the immutability, completeness, and thread-safety trade-offs and picks the right tool per case.
Defines API guidance: constructors/named args as default, apply for mutable interop, real builders for complex immutable assembly; addresses safe publication.
## What apply fundamentally is `apply` configures an object that *already exists* and is *mutable*. It returns that same instance — no copy, no transformation. Everything that follows comes from that fact. ## Implication 1 — needs mutable state To set fields inside the block, the type needs `var` properties or setter methods. You cannot `apply`-configure an immutable type with only `val`s set at construction. So apply pushes you toward mutable models, which can leak and be changed later: ```kotlin class Config { var host: String? = null; var port: Int = 0 } val c = Config().apply { host = "x" } // c is still mutable; c.port stays 0 ``` ## Implication 2 — no required-field enforcement With apply, forgetting to set a field is *not* a compile error; the field keeps its default (often `null` or `0`). Contrast Kotlin's **primary constructor with named + default arguments**, which makes required fields compile-time mandatory and optionals defaulted: ```kotlin data class Config(val host: String, val port: Int = 80) // host required by the compiler val c = Config(host = "x") // can't forget host ``` This is why, in idiomatic Kotlin, the *first* choice for a 'builder' is often just a constructor with named/default args, not apply. ## Implication 3 — no immutability / defensive copy apply returns the live, mutable object; callers can keep configuring it. If you want an immutable result, you need either an immutable type built via constructor, or a real builder whose `build()` returns an immutable instance. apply gives neither. ## Implication 4 — not thread-safe Mutating an object with apply and then sharing it across threads without proper publication (e.g., `@Volatile`, synchronization, or building it fully before handing off) can race: another thread may see a partially configured object. apply adds no synchronization. Build-then-publish (construct fully, then share an immutable reference) is the safe pattern; apply over a shared mutable object is not. ## When apply is the right tool - Configuring inherently mutable objects: `StringBuilder`, `java.util.Properties`, Java/framework config beans, mutable builders before `.build()`. - Local, single-threaded construction where the result is used immediately. - Reducing setter noise (see Java interop). ## When to reach for something else | Need | Better choice | |------|---------------| | Required fields enforced at compile time | primary constructor + named args | | Immutable result | `data class` via constructor, or builder returning immutable type | | Validation of invariants | `init { require(...) }` or a factory function | | Complex conditional assembly + immutable output | dedicated builder / type-safe DSL with `build()` | | Thread-safe shared config | build fully, publish immutable reference | ## Bottom line apply is the *simplest* receiver-lambda builder and excellent for in-place configuration of mutable objects, but it trades away immutability, completeness guarantees, and thread-safety. Reach for constructors with named/default arguments or a real builder when those guarantees matter.
- Why do many Kotlin APIs skip the builder pattern that Java uses?Kotlin's primary constructor with default and named arguments covers most of what Java builders do — readable call sites, optional params, required-field enforcement — and yields an immutable result without a separate builder class.
- How would you enforce that a required field is set when using an object configured by apply?apply can't enforce it; move the required field into the constructor, or add an init { require(field != null) } / a validating factory that throws if it's missing.
saying these in an interview costs you the question
- Claiming apply produces an immutable object
- Saying apply guarantees all required fields are set
- Treating apply-configured shared objects as thread-safe
- Always reaching for apply instead of a constructor with named args
- Not recognizing apply needs mutable (var/setter) state