skip to content

How Kotlin Builds Internal DSLs

A lambda with receiver hands the builder to the block as an implicit this, so nested calls read as declarative configuration while remaining ordinary, type-checked function calls. That single mechanism explains every Kotlin DSL you have used.

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

questions

5

What single Kotlin language feature makes type-safe internal DSLs (like the kotlinx.html `html { body { ... } }` builder) possible, and what does it do?

level: juniorimportance: must knowfreq 70%

answer

  1. A.() -> Unit = lambda with receiver
  2. Implicit this = the receiver instance
  3. buildString / apply / with use it
  4. Trailing lambda makes it read declaratively
  5. Each nested block = new receiver

basics

~20 s

A lambda with a receiver. The lambda type names a type before the dot, so inside the lambda that type becomes 'this'. You can call its methods directly, which makes nested config blocks read like a mini language.

solid answer

~40 s

The key feature is the function type with a receiver, written `A.() -> Unit`. When a function takes such a lambda, the block runs with an instance of `A` as the implicit `this` (the receiver). Inside the block you call `A`'s members and extensions without any qualifier, so `html { body { p { +"hi" } } }` is just nested calls each scoped to a different receiver. This is what people mean by an internal DSL: it's plain Kotlin, but receiver lambdas plus infix functions, operator overloads (like `unaryPlus`), and trailing-lambda syntax make it read declaratively. Standard-library examples are `apply`, `with`, and `buildString`, all of which use receiver lambdas.

code

kotlin · 7 lines
kotlin
fun buildString(builder: StringBuilder.() -> Unit): String =
    StringBuilder().apply(builder).toString()

val greeting = buildString {
    append("Hello, ")   // 'this' is StringBuilder
    append("DSL!")
}

go deeper

for a junior

Knows the syntax A.() -> Unit and that inside the block this is the receiver, can read a builder.

for a middle

Can write a small builder function that invokes a receiver lambda and explain trailing-lambda sugar.

for a senior

Explains nesting (each block = new receiver) and the supporting operators (unaryPlus, infix) that make it declarative.

for a principal

Frames internal vs external DSLs, discusses when a DSL pays off versus a plain config object, and notes @DslMarker for scope safety.

## The core idea An **internal DSL** is a domain-specific 'language' that is actually just normal library code in the host language (here Kotlin). Nothing is parsed specially — the compiler sees ordinary function calls — but the code *reads* like a declarative configuration language. The one feature that makes this possible in Kotlin is the **lambda with receiver**, also called a **function type with receiver**. ## Function type with receiver: `A.() -> Unit` A normal lambda type is `(Int) -> String`. A **receiver** type adds a type before the dot: ```kotlin val block: StringBuilder.() -> Unit = { append("hi") } ``` Here `block` is a lambda that, when invoked, runs *as if it were a method on a `StringBuilder`*. The `StringBuilder` instance becomes the **implicit `this`** (the receiver) inside the lambda body, so you can call `append(...)` directly — no `sb.append`. ## How a builder function uses it A builder function declares a parameter of this type and invokes the lambda on an object it created: ```kotlin fun buildString(builderAction: StringBuilder.() -> Unit): String { val sb = StringBuilder() sb.builderAction() // run the lambda with sb as receiver return sb.toString() } val s = buildString { // trailing-lambda syntax append("Hello, ") // 'this' is the StringBuilder append("world") } ``` Because the lambda is the last argument, Kotlin lets you write it as a trailing `{ ... }` block. Combined with the implicit receiver this looks like a configuration language. ## Why nesting works Each nested builder takes its *own* receiver lambda, so each `{ }` introduces a new implicit `this`: ```kotlin html { // this: HTML body { // this: BODY p { // this: P +"text" // unaryPlus operator on P adds a text node } } } ``` ## Other features that polish the DSL - **`operator fun unaryPlus()`** — lets `+"text"` mean 'add this text'. - **`infix fun`** — lets `"a" to 1` or `width equalTo 10` read like English. - **Trailing lambdas + default args** — keep call sites clean. - **`@DslMarker`** — restricts implicit receivers so you can't accidentally call an outer builder from an inner scope (covered in builder-pattern questions). The takeaway: the *enabling* feature is the receiver lambda `A.() -> Unit`; everything else is sugar on top.

  • How is `A.() -> Unit` different from `(A) -> Unit`?
    Both can be called with an `A`, but with the receiver type `A` is the implicit `this` (call members unqualified); with the parameter type `A` is an explicit named argument (e.g. `it`). The receiver form is what enables the DSL look.
  • Name a standard-library function built on a receiver lambda.
    `apply` (`T.(T.() -> Unit): T`), `with`, `run`, and `buildString` / `buildList` all use receiver lambdas.

It's like handing someone a clipboard already clipped to a specific form — they just start filling fields ('this' form) without re-stating which form each time.

saying these in an interview costs you the question

  • Claiming Kotlin parses DSLs specially or has a 'dsl' keyword
  • Confusing `A.() -> Unit` with `(A) -> Unit`
  • Saying the receiver is passed as `it`
  • Not being able to name any concrete builder (apply/buildString)

context

open as a page

Compare `fun config(block: Server.() -> Unit)` with `fun config(block: (Server) -> Unit)`. How does each change the call site, and why does the DSL community prefer the first?

level: middleimportance: must knowfreq 60%

basics

~20 s

With the receiver version you write member calls directly inside the block. With the parameter version you must prefix every call with the lambda argument, usually it. The receiver version reads cleaner, so DSLs use it.

open as a page

Beyond the receiver lambda, which Kotlin features turn nested builder calls into something that reads like a declarative language? Illustrate with `infix` and `unaryPlus`.

level: middleimportance: should knowfreq 45%

basics

~20 s

Infix functions let you drop the dot and parentheses, so a to b reads like words. Operator functions like unaryPlus let +"text" mean an action. Together with receiver lambdas they make blocks read like configuration sentences.

open as a page

Design a minimal type-safe builder DSL for an HTML-like structure (`html { head { title { +"x" } } body { p { +"y" } } }`). Walk through how receiver lambdas, nesting, and node collection work.

level: seniorimportance: should knowfreq 40%

basics

~20 s

Make a class per tag holding children. Each tag has methods that take a receiver lambda, create a child, run the lambda on it, add it to the children, and return it. A top-level function starts the tree. Text uses a unaryPlus operator.

open as a page

What is the runtime cost of all these nested receiver lambdas in a Kotlin DSL, and how do `inline` (and sometimes `reified`) change the picture?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

A lambda is normally a small object created at runtime. If a builder function is inline, the compiler copies the lambda's code into the call site, so no lambda object is allocated. That makes deeply nested DSLs basically free.

open as a page