skip to content

Builder Patterns

The concrete builder shapes: a function that creates an object and applies a receiver lambda, nesting those to mirror a tree, and the stdlib build functions. This is where DSL theory becomes code you can write on a whiteboard.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

20

What does the apply scope function do, and why is it convenient for configuring an object right after you create it?

level: juniorimportance: must knowfreq 78%

answer

  1. Receiver block T.() -> Unit
  2. Returns this (the receiver)
  3. Configure-in-place, no temp val
  4. this not it (vs also)
  5. inline, works on any type

basics

~10 s

apply runs a block on an object, lets you set its properties using 'this', and returns that same object. So you can create something and configure it in one expression.

solid answer

~40 s

apply is a Kotlin scope function declared as fun <T> T.apply(block: T.() -> Unit): T. Inside the block the object is the receiver (this), so you reference properties and methods directly without a prefix. The block returns Unit, but apply itself returns the receiver object. That makes it ideal for the builder idiom: Request().apply { url = u; header("k", v) } creates a Request, mutates it in place, and yields the configured Request as the value of the expression. It avoids a temporary val plus repeated references, keeps configuration grouped, and works on any type (no special interface needed). Compare with also, where the object is the argument it instead of the receiver.

code

kotlin · 13 lines
kotlin
data class Request(
    var url: String = "",
    var method: String = "GET"
) {
    val headers = mutableMapOf<String, String>()
    fun header(k: String, v: String) { headers[k] = v }
}

val request = Request().apply {
    url = "https://api.example.com"
    method = "POST"
    header("Accept", "application/json")
} // request is the fully configured Request

go deeper

for a junior

Knows apply runs a block with this as the object and returns that object; can write Request().apply { ... }.

for a middle

Contrasts apply with also/let and explains the receiver vs argument distinction and the Unit-vs-receiver return.

for a senior

Notes apply is inline, works on any T, and frames it as the simplest receiver-lambda builder; picks it deliberately over also/let.

for a principal

Discusses when this terse idiom helps or hurts readability/API design and when a real type-safe builder is warranted instead.

## What apply is `apply` is one of Kotlin's standard *scope functions* (alongside `let`, `run`, `with`, `also`). Its signature in the standard library is: ```kotlin public inline fun <T> T.apply(block: T.() -> Unit): T { block() return this } ``` Two things define its behavior: - **Receiver block (`T.() -> Unit`)** — the lambda has the object as its *receiver*. Inside the block, `this` is the object, so you can write `url = u` or `header("k", v)` directly, with no `it.`/`obj.` prefix. - **Returns the receiver (`: T`)** — the block's own result (`Unit`) is discarded; `apply` returns the same object it was called on. This is what makes it chainable and usable as an expression. ## The builder idiom Because `apply` returns the configured object, you can construct-and-configure in a single expression: ```kotlin val request = Request().apply { url = "https://api.example.com" // this.url method = "POST" header("Accept", "application/json") // this.header(...) } ``` This is the simplest *receiver-lambda builder*: no dedicated builder class, no interface, no `build()` call. Any mutable object works because `apply` is an extension on `T` (the generic type), available on everything. ## Why it's convenient - **No temporary variable** — you don't need `val r = Request(); r.url = ...; r.method = ...` and then return `r`. - **Grouped configuration** — all the setup lives in one visually scoped block. - **Direct member access** — receiver scope means terse `name = ...` instead of `r.name = ...`. - **Inlined** — `apply` is an `inline` function, so there is no lambda-allocation or call overhead at runtime. ## apply vs also `also` has signature `fun <T> T.also(block: (T) -> Unit): T`. It also returns the receiver, but the object is passed as the *argument* `it`, not as `this`. Use `apply` to **configure** (`this.x = ...`); use `also` for **side effects** that read the object (`also { log(it) }`).

  • What does the apply block itself return, and does that matter?
    The block returns Unit, which is discarded. apply always returns the receiver, so the last expression in the block is irrelevant to apply's result.
  • When would you choose also instead of apply?
    When the lambda is a side effect that reads the object (logging, validation, registering it) rather than configuring it — also exposes the object as it, keeping the call's intent (peek/side-effect) clear.

Like buying flat-pack furniture and assembling it before handing the finished piece to someone — you get the same item back, just set up.

saying these in an interview costs you the question

  • Saying apply returns the result of the last line in the block
  • Confusing apply (this) with also (it)
  • Thinking the object must implement a special builder interface
  • Claiming apply mutates a copy rather than the original object
  • Saying apply only works on data classes

context

open as a page

What is a nested builder DSL in Kotlin (e.g. html { body { p { } } }), and what does an outer builder function like tr { td { } } actually do under the hood?

level: juniorimportance: must knowfreq 55%

basics

~20 s

A nested builder lets you describe a tree by writing blocks inside blocks. Each outer function creates a child object, runs the block you pass to fill it in, then stores that child in its parent.

open as a page

What does the stdlib `buildString { ... }` function do, and how is it different from concatenating strings with `+` in a loop?

level: juniorimportance: must knowfreq 70%

basics

~20 s

buildString gives you a temporary text buffer you fill inside a lambda, then hands back the finished String. It reuses one buffer, so it's faster than + in a loop, which makes a brand-new string each time.

open as a page

What is a type-safe builder in Kotlin, and what does a builder function like `fun table(init: Table.() -> Unit): Table` actually do?

level: juniorimportance: must knowfreq 55%

basics

~20 s

A function that creates an object, lets you configure it inside a block, and returns it. The compiler checks your configuration code, so typos and wrong calls fail to compile instead of breaking at runtime.

open as a page

Compare apply with let, run, with, and also: which provide a receiver (this) vs an argument (it), and which return the receiver vs the lambda result?

level: middleimportance: must knowfreq 72%

basics

~20 s

apply and run use 'this'; let and also use 'it'. apply and also give you the object back; let and run give you the lambda's result. with is like run but takes the object as an argument.

open as a page

Walk through exactly how the receiver lambda makes td { text = "x" } resolve text against the child node, and show how to make an apply-based one-liner builder.

level: middleimportance: must knowfreq 50%

basics

~20 s

The block you pass is a lambda whose 'this' is the child node. So inside td { }, text means the child's text. You can write the builder in one line with apply, which runs the block on the child and returns it.

open as a page

Explain `buildList`, `buildSet`, and `buildMap`. What is the type of the receiver inside the block, and what type is returned?

level: middleimportance: must knowfreq 60%

basics

~10 s

Each gives you a temporarily editable collection inside the lambda (you call add, put, etc.), and when the block ends it hands back a normal read-only List, Set, or Map.

open as a page

Inside `table { row { } }`, how does the compiler know that `row` belongs to `Table`? Explain how the receiver lambda resolves calls.

level: middleimportance: must knowfreq 50%

basics

~10 s

The block passed to table runs with a Table as its this. So an unqualified call like row is looked up as a member of Table, exactly as if you wrote thisTable.row.

open as a page

How does apply help when configuring Java objects that use setter methods or builders from Kotlin?

level: middleimportance: should knowfreq 55%

basics

~10 s

apply lets you call all the Java setters in one block on the object and get it back, so configuring a verbose Java object becomes one clean expression instead of many repeated variable references.

open as a page

Compare two ways to add children in a nested DSL: a receiver-lambda builder (tr { td {} td {} }) versus passing children as varargs/operators. When would you choose each?

level: middleimportance: should knowfreq 30%

basics

~20 s

With receiver lambdas you call a builder function for each child inside a block. With varargs you create the children first and pass them in. Use lambdas when children are configured inline; use varargs when children are simple values you already have.

open as a page

How do the `build*` functions compare to `mutableListOf().apply { }.toList()` or `StringBuilder().apply { }.toString()`? What concrete advantage do they add?

level: middleimportance: should knowfreq 45%

basics

~10 s

They do the same thing but in one step: create a mutable builder, let you fill it, and return a read-only result. buildList skips the extra .toList() copy and gives a cleaner, intention-revealing call.

open as a page

What concrete advantages does a type-safe builder have over generating the same output (e.g. HTML) with string templates or concatenation?

level: middleimportance: should knowfreq 40%

basics

~10 s

The compiler checks the builder: wrong tags, missing values, or bad types fail to compile. String templates only break at runtime, and you get autocomplete, refactoring, and automatic escaping with the builder instead.

open as a page

When is apply the wrong tool for a builder, and what does configuring a mutable object in place imply for immutability, nullability, and concurrency?

level: seniorimportance: should knowfreq 40%

basics

~20 s

apply 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.

open as a page

What pitfalls arise from nesting apply blocks or referencing the outer scope, and how do labels and this resolution behave?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Inside nested apply blocks, 'this' refers to the innermost object, which can shadow the outer one. You use labeled this, like this@Outer, to reach an enclosing receiver, and be careful that unqualified names resolve to the nearest scope.

open as a page

When building nested builders like table { tr { td { } } }, what problem can arise with multiple receivers in scope, and how does @DslMarker solve it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Inside a deeply nested block, the outer node is still in scope, so you can accidentally call an outer builder from the wrong place (like adding a row from inside a cell). @DslMarker blocks that by allowing only the nearest receiver implicitly.

open as a page

A teammate captures the builder reference out of `buildList` and mutates it after the block returns. Why is this a bug, and what does the contract guarantee?

level: seniorimportance: should knowfreq 30%

basics

~20 s

The build* functions only promise the result is read-only because you stop touching the builder when the block ends. If you keep a reference and mutate it afterward, you can silently change a list everyone thinks is frozen — undefined behavior.

open as a page

In a nested type-safe builder, why can inner blocks accidentally call outer-receiver methods, and how does `@DslMarker` fix it?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Because outer receivers stay in scope inside nested blocks, you can mistakenly call an outer builder's method from a deeply nested one. @DslMarker tells the compiler to hide outer receivers of the same DSL unless you name them explicitly.

open as a page

When should you pass a `capacity` to `buildList`/`buildString`, and what is the performance characteristic of building a collection or string this way?

level: seniorimportance: nice to knowfreq 22%

basics

~10 s

Pass capacity when you already know roughly how many items or characters you'll add. It pre-sizes the internal array so it doesn't keep resizing and copying as it grows, which speeds up big builds.

open as a page

When designing a builder entry function `fun table(init: Table.() -> Unit): Table`, what design choices (inline, return value, non-null result, immutability) matter, and why?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Decide whether to inline the function (cheaper lambdas, allows non-local returns), make sure it returns the configured object (so it composes), and choose whether the result is mutable afterward. These choices affect performance, ergonomics, and safety.

open as a page

You're designing a public nested builder DSL that produces an immutable tree. What design choices prevent builder/mutation leaks and partially-built nodes from escaping?

level: principalimportance: nice to knowfreq 22%

basics

~10 s

Keep mutable builders hidden, build into private mutable lists, and only hand back a finished read-only tree. Don't return the builder or let the configuration block run after building finishes.

open as a page