Building DSLs (Lambdas with Receiver)

Reviewed & published by Brayan K

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.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, 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().

// A lambda WITH RECEIVER has type T.() -> Unit.
// Inside the lambda, 'this' is a T — so you call T's members directly.
class Greeter {
    var name = ""
    fun shout() = println("HELLO ${name.uppercase()}!")
}

// The parameter type 'Greeter.() -> Unit' means: a lambda whose
// receiver ('this') is a Greeter.
fun greeting(block: Greeter.() -> Unit): Greeter {
    val g = Greeter()
    g.block()           // run the lambda with g as 'this'
    return g
}

fun main() {
    greeting {
        name = "Ada"    // 'this' is the Greeter, so no prefix needed
        shout()         // calls Greeter.shout()
    }
}

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.

// buildString is a DSL from the standard library:
// it gives you a StringBuilder as 'this' and returns the built String.
fun main() {
    val report = buildString {
        append("Sales: ")     // 'this' is a StringBuilder
        append(100)
        appendLine()          // newline
        append("Done")
    }
    println(report)
}

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.

// A tiny HTML builder DSL using lambdas with receiver.
class Tag(val name: String) {
    private val children = StringBuilder()

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

    fun tag(name: String, block: Tag.() -> Unit) {
        val child = Tag(name)
        child.block()
        children.append(child.render())
    }

    fun render(): String = "<$name>$children</$name>"
}

fun html(block: Tag.() -> Unit): String {
    val root = Tag("div")
    root.block()
    return root.render()
}

fun main() {
    val page = html {
        tag("p") { text("Hi") }
    }
    println(page)
}

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.

// 🎯 YOUR TURN — replace each ___ then run main()
class Config {
    var title = ""
    var debug = false
}

// 1) The block parameter should be a lambda with Config as receiver
fun config(block: Config.___ Unit): Config {   // 👉 put: () ->
    val c = Config()
    c.block()
    return c
}

fun main() {
    val c = config {
        title = "App"     // 'this' is the Config
        debug = true
    }
    println("${c.title} debug=${c.debug}")

    // ✅ Expected output:
    // App debug=true
}

Mini-Challenge: A Menu DSL

Build a menu { } DSL that collects items and prints them joined.

// 🎯 MINI-CHALLENGE: a tiny menu DSL.
// Save as Menu.kt and run main().
//
// 1. Menu has a fun item(name: String) that adds to a list.
// 2. menu { } takes a Menu.() -> Unit, runs it, returns the Menu.
// 3. Print the joined items separated by ", ".
//
// ✅ Expected output:
//    Soup, Salad, Cake

class Menu {
    val items = mutableListOf<String>()
    fun item(name: String) { items.add(name) }
}

fun menu(block: Menu.() -> Unit): Menu {
    val m = Menu()
    m.block()
    return m
}

fun main() {
    val m = menu {
        item("Soup")
        item("Salad")
        item("Cake")
    }
    // print the items joined by ", "
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — DSL Building Blocks

ConceptKotlin
Receiver lambda typeT.() -> Unit
Inside the blockthis is a T
Run the blockreceiver.block()
Configure + returnobj.apply { ... }
Build a StringbuildString { ... }
Protect nesting@DslMarker

🎉 Lesson Complete!

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

Frequently asked questions

What is a lambda with receiver?

A lambda with receiver has a function type written T.() -> R, where T is the receiver type. Inside such a lambda, 'this' refers to an instance of T, so you can call T's members directly without any prefix — exactly as if you were writing code inside the class. This is the single feature that powers Kotlin DSLs: it lets a builder block look like a mini-language where method and property calls read naturally. The standard library's apply, run (the receiver form), and buildString all use it.

How is T.() -> Unit different from (T) -> Unit?

With (T) -> Unit the value is an ordinary parameter — you receive it as 'it' (or a named parameter) and must write it.member to use it. With T.() -> Unit the value becomes the receiver 'this', so you write member directly with no prefix. The two are otherwise equivalent in power, but the receiver form is what makes builder blocks read cleanly: inside html { } you can call tag(...) as if it were a top-level function, because 'this' is the Tag.

What is apply and why is it a DSL?

apply is a scope function defined roughly as fun <T> T.apply(block: T.() -> Unit): T. It runs the block with the object as receiver and then returns that same object. Because the block is a lambda with receiver, you can configure an object inline — person.apply { name = "Ada"; age = 30 } — and still get the configured person back. It is the simplest example of the builder pattern Kotlin DSLs are built on, and it is perfect for setting several properties right after construction.

How do I build a DSL like the HTML example?

Define a builder class whose methods add children, then write a top-level function that creates a root builder, invokes a T.() -> Unit block on it, and returns the result. Each nested method that itself takes a T.() -> Unit creates a child builder and runs its block the same way, so blocks nest naturally: html { tag("p") { text("Hi") } }. The receiver in each level is the appropriate builder, which is why inner calls need no prefix. Kotlin's kotlinx.html library scales exactly this idea.

What does @DslMarker do?

When you nest builders, a problem appears: inside an inner block, both the inner and outer receivers are in scope, so you could accidentally call an outer method where only an inner one makes sense. @DslMarker fixes this. You create your own annotation marked with @DslMarker and apply it to your builder classes; the compiler then forbids calling a method on an implicit outer receiver from within a more nested block, requiring an explicit reference instead. It makes DSLs far less error-prone, which is why HTML and Gradle DSLs use it.