Building DSLs (Lambdas with Receiver)
A lambda with receiver has the type T.() -> Unit , which makes this inside the block a T — the one feature that lets you build clean, readable domain-specific languages.
Learn Building DSLs (Lambdas with Receiver) in our free Kotlin course — a beginner-friendly interactive lesson with worked examples, a practice exercise and…
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
You'll use apply and buildString as DSLs, build a tiny HTML builder, and learn what @DslMarker protects you from.
What You'll Learn in This Lesson
1️⃣ Lambdas With Receiver
A normal lambda parameter is (T) -> Unit . Add a receiver and it becomes T.() -> Unit : now inside the lambda, this is a T , so you call its members with no prefix. You invoke such a block with receiver.block() .
2️⃣ apply and buildString as DSLs
The standard library already ships receiver-based DSLs. apply runs a block with an object as this and returns it, perfect for configuration. buildString gives you a StringBuilder as this and returns the finished String .
3️⃣ A Tiny HTML Builder
Put it together: a builder class with methods that add children, plus a top-level entry function that runs a T.() -> Unit block. Each nested method that itself takes a receiver block creates a child builder, so the blocks nest and read like markup.
Real libraries like kotlinx.html and the Gradle Kotlin DSL are exactly this pattern, scaled up — and they use @DslMarker to keep nested receivers from clashing.
Your turn. Fill in the ___ blank, then run and compare.
Mini-Challenge: A Menu DSL
Build a menu { } DSL that collects items and prints them joined.
Common Errors (and the fix)
- ❌ "Unresolved reference" inside the block — you used (T) -> Unit , so members aren't in scope. ✅ Use T.() -> Unit to make them the receiver.
- ❌ Forgetting to call the block — your builder runs nothing. ✅ Invoke it with receiver.block() .
- ❌ Forgetting to return the receiver — the caller gets nothing back. ✅ Return the built object (like apply does).
- ❌ Calling an outer method by accident in a nested block — ambiguous receivers. ✅ Add @DslMarker to your builder types.
Pro Tips
- 💡 Start with apply: most "I want a builder" needs are met by apply before you write a full DSL.
- 💡 Return this for chaining: have builder methods return the receiver if you want fluent .a().b() chains too.
- 💡 Mark your DSL: always add @DslMarker once a DSL has more than one level of nesting.
📋 Quick Reference — DSL Building Blocks
Concept
Kotlin
Receiver lambda type
T.() -> Unit
Inside the block
this is a T
Run the block
receiver.block()
Configure + return
obj.apply { ... }
Build a String
buildString { ... }
Protect nesting
@DslMarker
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ T.() -> Unit makes this the receiver inside a block
- ✅ Members of T are callable with no prefix — the heart of DSLs
- ✅ apply and buildString are receiver-based DSLs you already use
- ✅ Nested receiver blocks let builders read like markup
- ✅ @DslMarker stops accidental calls on outer receivers
- ✅ Next lesson: Result & runCatching
Practice quiz
What is the type of a lambda with receiver?
- T.() -> R
- (T) -> R
- () -> T
- T -> R
Answer: T.() -> R. A lambda with receiver has type T.() -> R, where T is the receiver type.
Inside a lambda with receiver of type T, what does 'this' refer to?
- The enclosing function
- Always null
- An instance of T
- The first parameter named it
Answer: An instance of T. Inside a T.() -> Unit lambda, this is an instance of T, so members are called with no prefix.
How does T.() -> Unit differ from (T) -> Unit?
- They are identical in every way
- T.() -> Unit gives you 'this'; (T) -> Unit gives you a parameter like 'it'
- (T) -> Unit cannot be called
- T.() -> Unit takes no receiver
Answer: T.() -> Unit gives you 'this'; (T) -> Unit gives you a parameter like 'it'. The receiver form exposes the value as this; the parameter form exposes it as it.
What does the standard library's apply function return?
- The same object it was called on
- A new copy
- Unit
- A Boolean
Answer: The same object it was called on. apply runs the block with the object as receiver and returns that same object.
What does buildString give you as 'this' inside its block?
- A List
- A StringBuilder
- a Map
- an Int
Answer: A StringBuilder. buildString provides a StringBuilder as this and returns the finished String.
To run a receiver block on a receiver, you write...
- block(receiver)
- receiver -> block
- call(block)
- receiver.block()
Answer: receiver.block(). You invoke a receiver lambda with receiver.block().
What does @DslMarker prevent?
- Calling a method on an implicit outer receiver from a nested block
- Defining more than one builder class
- Using lambdas at all
- Returning a value from a DSL
Answer: Calling a method on an implicit outer receiver from a nested block. @DslMarker forbids accidental calls on an implicit outer receiver inside nested blocks.
Why do nested builder blocks read naturally like html { tag("p") { ... } }?
- Because of reflection
- Because each block's receiver is the appropriate builder, so inner calls need no prefix
- Because Kotlin shuffles the calls
- Because builders are global functions
Answer: Because each block's receiver is the appropriate builder, so inner calls need no prefix. Each level's receiver is the relevant builder, so its members are callable without a prefix.
What does a top-level DSL entry function typically do?
- Creates a root builder, runs a T.() -> Unit block on it, and returns the result
- Only prints text
- Throws an exception
- Returns Unit and ignores the block
Answer: Creates a root builder, runs a T.() -> Unit block on it, and returns the result. It builds a root, invokes the receiver block, and returns the built object.
Which standard-library function is the simplest example of a builder-style receiver DSL?
- println
- apply
- listOf
- require
Answer: apply. apply is the simplest receiver-based builder: configure inline and get the object back.
Continue this course
- Previous: Generic Variance (in/out)
- Next: Result & runCatching