Phase 5 · Advanced KotlinModule 28~36 min read

Domain-Specific Languages (DSLs)

Build expressive, type-safe DSLs using lambdas with receivers — a Kotlin superpower.

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:

DSLs you'll meet in Kotlin

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

You've met a lambda with a receiver before: 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:

HtmlDsl.kt
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

For nested DSLs, the @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

This is why Kotlin dominates Android UI and build scripts: a well-designed DSL makes complex configuration readable and type-safe — you get auto-complete and compile errors, unlike a config file written in YAML or XML.

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.