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 pageshowhide
explore
- How Kotlin Builds Internal DSLs5 questions
- Receiver Lambdas20 questions
- Lambda with Receiver A.()->Unit5 questions
- Implicit Receiver Resolution5 questions
- @DslMarker Scope Control5 questions
- Multiple Receivers & Context Parameters5 questions
- Builder Patterns20 questions
- Type-Safe Builders5 questions
- Nested Builder DSLs5 questions
- apply-Based Builders5 questions
- DSL Examples & Idioms21 questions
- HTML / Markup DSL Pattern5 questions
- Gradle Kotlin DSL6 questions
- Routing & Config DSL Idioms5 questions
- Operator & infix DSL Composition5 questions
questions
66 · 4 sectionsWhat single Kotlin language feature makes type-safe internal DSLs (like the kotlinx.html `html { body { ... } }` builder) possible, and what does it do?
basics
~20 sA 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.
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?
basics
~20 sWith 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.
Beyond the receiver lambda, which Kotlin features turn nested builder calls into something that reads like a declarative language? Illustrate with `infix` and `unaryPlus`.
basics
~20 sInfix 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.
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.
basics
~20 sMake 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.
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?
basics
~20 sA 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.
What problem does the @DslMarker annotation solve in a Kotlin type-safe builder DSL?
basics
~10 sIn 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.
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?
basics
~20 sA 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.
What is a function type with receiver like A.() -> Unit, and how does it differ from a plain (A) -> Unit?
basics
~10 sIt 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'.
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?
basics
~20 sNest 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.
Show how to define and apply a @DslMarker annotation. Where exactly must the marker be placed for the scope restriction to take effect?
basics
~10 sCreate 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.
What does the apply scope function do, and why is it convenient for configuring an object right after you create it?
basics
~10 sapply 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.
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?
basics
~20 sA 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.
What does the stdlib `buildString { ... }` function do, and how is it different from concatenating strings with `+` in a loop?
basics
~20 sbuildString 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.
What is a type-safe builder in Kotlin, and what does a builder function like `fun table(init: Table.() -> Unit): Table` actually do?
basics
~20 sA 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.
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?
basics
~20 sapply 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.
Explain the roles of the plugins {} and dependencies {} blocks in build.gradle.kts. How do you apply a plugin and declare an implementation dependency?
basics
~10 splugins {} 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").
What is build.gradle.kts and how does it differ from a build.gradle file?
basics
~10 sbuild.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.
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?
basics
~10 sEach 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.
What is an infix function in Kotlin, and what are the rules for declaring one? Use it to explain how `1 to "a"` works.
basics
~20 sAn 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.
Explain how a config DSL like Json { isLenient = true } works under the hood. What two Kotlin language features make this `{ ... }` block possible?
basics
~10 sThe 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.