skip to content

DSLs & Type-Safe Builders

How Kotlin builds internal DSLs: lambdas with receivers, scope control with @DslMarker, and type-safe builders. Interviewers use DSL questions to see whether you understand receivers deeply rather than just using Gradle and apply.

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

explore

questions

66 · 4 sections

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

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

What problem does the @DslMarker annotation solve in a Kotlin type-safe builder DSL?

level: juniorimportance: must knowfreq 55%
basics
~10 s

In a nested builder block, methods from the outer block are still callable by accident. @DslMarker hides the outer receiver inside inner blocks, so you only see the members that belong where you are.

open as a page

In a type-safe builder like an HTML DSL, what is an "implicit receiver" and why can you call functions like body { ... } inside html { ... } without writing any object name before them?

level: juniorimportance: must knowfreq 60%
basics
~20 s

A lambda with a receiver runs as if its body were inside a special object. That object is the 'implicit receiver', so its members can be called by name alone, with no dot before them.

open as a page

What is a function type with receiver like A.() -> Unit, and how does it differ from a plain (A) -> Unit?

level: juniorimportance: must knowfreq 70%
basics
~10 s

It is a lambda that runs as if it lives inside an object of type A. Inside the block, you can call A's methods directly without naming the object, because A becomes 'this'.

open as a page

How do you make two receivers available inside the same block using only standard scope functions, e.g. with(a) { b.apply { ... } }, and which receiver wins when both have a member with the same name?

level: juniorimportance: must knowfreq 55%
basics
~20 s

Nest scope functions: with(a) { b.apply { ... } }. Inside the inner block both a and b are reachable. If both define the same name, the innermost receiver (b) is used unless you qualify it.

open as a page

Show how to define and apply a @DslMarker annotation. Where exactly must the marker be placed for the scope restriction to take effect?

level: middleimportance: must knowfreq 45%
basics
~10 s

Create an annotation class and mark it with @DslMarker. Then put that annotation on the builder/receiver classes (or on the lambda's receiver type). The restriction kicks in when two implicit receivers share that marker.

open as a page

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

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

Explain the roles of the plugins {} and dependencies {} blocks in build.gradle.kts. How do you apply a plugin and declare an implementation dependency?

level: juniorimportance: must knowfreq 65%
basics
~10 s

plugins {} applies Gradle plugins that add build capabilities and tasks. dependencies {} lists the libraries your code needs, grouped by configuration like implementation or testImplementation. You add a library with implementation("group:name:version").

open as a page

What is build.gradle.kts and how does it differ from a build.gradle file?

level: juniorimportance: must knowfreq 70%
basics
~10 s

build.gradle.kts is a Gradle build script written in Kotlin instead of Groovy. Because it is real Kotlin code, the IDE gives autocomplete, type checking, and click-through navigation that the Groovy script cannot.

open as a page

In a kotlinx.html-style DSL like html { body { p { +"hi" } } }, what Kotlin language feature makes the nested blocks work, and why can each block call functions like body or p without a qualifier?

level: juniorimportance: must knowfreq 60%
basics
~10 s

Each block is a lambda that runs 'on' an object (a receiver). Inside the block you can call that object's functions directly, so body() and p() are methods of the current builder.

open as a page

What is an infix function in Kotlin, and what are the rules for declaring one? Use it to explain how `1 to "a"` works.

level: juniorimportance: must knowfreq 70%
basics
~20 s

An infix function lets you call it without a dot or parentheses, like 1 to "a". You mark it with the infix keyword. It must be a member or extension function with exactly one parameter and no default value.

open as a page

Explain how a config DSL like Json { isLenient = true } works under the hood. What two Kotlin language features make this `{ ... }` block possible?

level: juniorimportance: must knowfreq 75%
basics
~10 s

The braces are a function passed as the last argument. Inside, this is a settings object, so you set properties like isLenient directly without naming the object.

open as a page