What you'll learn
A DSL (Domain-Specific Language) is a mini-language for a particular task, expressed in Kotlin itself. Kotlin is exceptionally good at building them — it's why Gradle scripts, Jetpack Compose UIs, and HTML builders read so cleanly. The secret is one feature: lambdas with receivers.
By the end you'll be able to:
- Explain what a DSL is and why Kotlin excels at them
- Understand lambdas with a receiver (
T.() -> Unit) - Build a small, type-safe DSL
What is a DSL?
A DSL lets you write code that reads like the domain it describes, rather than generic programming. Kotlin's features — trailing lambdas, extension functions, operator overloading, and lambdas with receivers — combine so you can build APIs that feel like a purpose-built language. You've been using DSLs already:
Gradle Kotlin DSL
build.gradle.kts configuration
Jetpack Compose
declarative Android UI
kotlinx.html
type-safe HTML builders
Ktor routing
server route definitions
Lambdas with a receiver
This is the key ingredient. A normal lambda type is () -> Unit. A lambda with a receiver is written Builder.() -> Unit — inside such a lambda, this is a Builder, so you can call its members directly, without a prefix. That's what turns nested function calls into clean, block-structured configuration.
Note
apply { ... } from Module 10. Inside its block, this is the object being configured. DSLs take that same idea and build whole APIs around it.A tiny DSL
Let's build a mini HTML DSL. The html { ... } function takes a lambda with an HtmlBuilder receiver, so inside the block you call h1(...) and p(...) as if they were language keywords:
class HtmlBuilder {
private val sb = StringBuilder()
fun h1(text: String) { sb.append("<h1>$text</h1>") }
fun p(text: String) { sb.append("<p>$text</p>") }
override fun toString() = sb.toString()
}
// the lambda's receiver is HtmlBuilder, so h1()/p() are called directly on it
fun html(block: HtmlBuilder.() -> Unit): String =
HtmlBuilder().apply(block).toString()
val page = html {
h1("Welcome")
p("Hello, Kotlin!")
}
println(page)Tip
@DslMarker annotation prevents you from accidentally calling an outer builder's methods from an inner block — keeping the DSL well-scoped. Real DSLs like Compose and kotlinx.html use it heavily.Key idea
Recap & quick check
Key takeaways
- A DSL is a mini-language for a domain, expressed in Kotlin (Gradle, Compose, kotlinx.html).
- The key feature is a lambda with a receiver: Builder.() -> Unit, where 'this' is the Builder.
- Inside such a lambda you call the receiver's members directly, with no prefix — like block-structured config.
- apply { } is a familiar lambda-with-receiver; DSLs generalise the idea.
- @DslMarker keeps nested DSLs well-scoped; DSLs give you type safety and auto-complete.
Quick check
1. What is a DSL?
2. What is the key feature behind Kotlin DSLs?
3. Inside a lambda with an HtmlBuilder receiver, how do you call h1()?
4. Which familiar function is a lambda with a receiver?
5. What does @DslMarker do?
Excellent — you understand the magic behind Compose and Gradle. Next up: Module 29 — Functional Kotlin, the finale of Phase 5.