Type-Safe Builders and DSLs

This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

A lambda with receiver is the single mechanism behind every "DSL-looking" Kotlin API — build.gradle.kts, kotlinx.html, Ktor’s routing block — and it is worth understanding in isolation from any one of those libraries.

Lambdas with Receiver

An ordinary function type is (T) → R; a function type with receiver is written T.() → R — inside such a lambda, members of T are callable directly (via an implicit this), exactly as if the lambda’s body were written inside a member function of T:

class HtmlBuilder {
    private val content = StringBuilder()

    fun text(value: String) {
        content.append(value)
    }

    override fun toString() = content.toString()
}

fun html(block: HtmlBuilder.() -> Unit): String {   // "block" has HtmlBuilder as its receiver
    val builder = HtmlBuilder()
    builder.block()                                   // calling the lambda AS IF it were a member of builder
    return builder.toString()
}

val page = html {
    text("Hello, ")     // "text" is called with no explicit receiver -- resolved against the implicit "this"
    text("World!")
}

This is exactly the same mechanism run/with/apply (Extension Functions and Scope Functions) use internally — a scope function’s lambda parameter is itself typed T.() → R.

Building a Small Type-Safe HTML Builder

Nesting builder classes, each exposing member functions that themselves take a lambda-with-receiver, produces a tree-shaped DSL where the Kotlin compiler enforces the structure (only a <li> inside a <ul>, say) at compile time — unlike a plain string-templating approach, which catches nothing until the output is rendered:

class Ul {
    private val items = mutableListOf<String>()
    fun li(text: String) { items += "<li>$text</li>" }
    override fun toString() = "<ul>${items.joinToString("")}</ul>"
}

class Html {
    private val parts = mutableListOf<String>()
    fun ul(block: Ul.() -> Unit) { parts += Ul().apply(block).toString() }
    override fun toString() = parts.joinToString("")
}

fun html(block: Html.() -> Unit): String = Html().apply(block).toString()

val page = html {
    ul {
        li("first")
        li("second")
    }
}
// "<ul><li>first</li><li>second</li></ul>"

Each nested block call chains to a new receiver (Ul, not Html) for its own body — li(…​) is only callable inside a ul { } block, which is the compile-time structural guarantee this style buys over generating markup with raw string concatenation.

flowchart TD A["html { ... }"] -->|receiver: Html| B["ul { ... }"] B -->|receiver: Ul| C["li('first')"] B -->|receiver: Ul| D["li('second')"] C --> E["Ul.toString()\n→ <ul><li>first</li>...</ul>"] D --> E E --> F["Html.toString()\n→ full page markup"]

Where This Pattern Shows Up

  • kotlinx.html — an HTML-generation library built on exactly this pattern, one builder class per HTML element.

  • The Gradle Kotlin DSL (build.gradle.kts) — plugins { }, dependencies { }, and tasks.named { } are all lambda-with-receiver blocks over Gradle’s own configuration objects, which is what gives build.gradle.kts IDE autocompletion a Groovy build.gradle never had.

  • Routing DSLs (e.g. Ktor’s routing { get("/users") { …​ } }) — the same nested-receiver structure, scoped to defining HTTP routes instead of markup.

See Also

References