Kotlin DSL Basics

A DSL (Domain-Specific Language) is a mini-language built inside Kotlin. You use Kotlin's lambda-with-receiver feature to create APIs that read like natural language for a specific domain. Gradle build scripts, Ktor routing, Jetpack Compose, and HTML builders are all Kotlin DSLs.

What Makes a DSL


Instead of:
  val body = Email()
  body.to = "alice@mail.com"
  body.subject = "Hello"
  body.body = "Hi Alice!"
  send(body)

A DSL lets you write:
  email {
      to = "alice@mail.com"
      subject = "Hello"
      body = "Hi Alice!"
  }

The second form reads like a configuration — clear, concise, and domain-specific.

Building Blocks: Lambda with Receiver

class EmailBuilder {
    var to: String = ""
    var subject: String = ""
    var body: String = ""
    var from: String = "noreply@system.com"

    fun build() = "To: $to\nFrom: $from\nSubject: $subject\n\n$body"
}

// DSL entry function — takes a lambda with receiver (EmailBuilder.() -> Unit)
fun email(configure: EmailBuilder.() -> Unit): String {
    val builder = EmailBuilder()
    builder.configure()   // 'this' inside the lambda is the EmailBuilder
    return builder.build()
}

fun main() {
    val message = email {
        to      = "alice@mail.com"
        subject = "Meeting Tomorrow"
        body    = "Hi Alice, please join us at 10am."
    }
    println(message)
}

Output:

To: alice@mail.com
From: noreply@system.com
Subject: Meeting Tomorrow

Hi Alice, please join us at 10am.

Lambda with Receiver Explained


fun email(configure: EmailBuilder.() -> Unit)
                     │
                     └── "Lambda with receiver"
                         Inside the lambda, 'this' is an EmailBuilder
                         So you can call to = ..., subject = ...
                         as if you were inside the EmailBuilder class

When you write:
  email {
      to = "alice@mail.com"   // this.to = "alice@mail.com"
  }

Kotlin expands it to:
  val b = EmailBuilder()
  b.to = "alice@mail.com"

Nested DSL

class TableBuilder {
    val rows = mutableListOf<RowBuilder>()

    fun row(configure: RowBuilder.() -> Unit) {
        rows.add(RowBuilder().also { it.configure() })
    }

    fun build(): String = rows.joinToString("\n") { it.build() }
}

class RowBuilder {
    val cells = mutableListOf<String>()

    fun cell(value: String) { cells.add(value) }
    fun build() = "| ${cells.joinToString(" | ")} |"
}

fun table(configure: TableBuilder.() -> Unit): String {
    return TableBuilder().also { it.configure() }.build()
}

fun main() {
    val output = table {
        row {
            cell("Name"); cell("Score"); cell("Grade")
        }
        row {
            cell("Alice"); cell("92"); cell("A")
        }
        row {
            cell("Bob"); cell("75"); cell("B")
        }
    }
    println(output)
}

Output:

| Name | Score | Grade |
| Alice | 92 | A |
| Bob | 75 | B |

Real-World DSL: HTML Builder

class HtmlBuilder {
    private val content = StringBuilder()

    fun h1(text: String) { content.appendLine("<h1>$text</h1>") }
    fun p(text: String)  { content.appendLine("<p>$text</p>") }
    fun ul(configure: UlBuilder.() -> Unit) {
        content.appendLine("<ul>")
        UlBuilder(content).configure()
        content.appendLine("</ul>")
    }
    fun build() = content.toString()
}

class UlBuilder(private val content: StringBuilder) {
    fun li(text: String) { content.appendLine("  <li>$text</li>") }
}

fun html(configure: HtmlBuilder.() -> Unit) =
    HtmlBuilder().also { it.configure() }.build()

fun main() {
    val page = html {
        h1("Kotlin Features")
        p("Kotlin is a modern JVM language.")
        ul {
            li("Null Safety")
            li("Coroutines")
            li("Extension Functions")
        }
    }
    print(page)
}

Output:

<h1>Kotlin Features</h1>
<p>Kotlin is a modern JVM language.</p>
<ul>
  <li>Null Safety</li>
  <li>Coroutines</li>
  <li>Extension Functions</li>
</ul>

Practical Example: Test Data Builder DSL

data class User(val name: String, val age: Int, val email: String, val role: String)

class UserDsl {
    var name:  String = "Test User"
    var age:   Int    = 25
    var email: String = "test@example.com"
    var role:  String = "member"
    fun build() = User(name, age, email, role)
}

fun user(configure: UserDsl.() -> Unit = {}) = UserDsl().also { it.configure() }.build()

fun main() {
    val alice = user { name = "Alice"; age = 30; role = "admin" }
    val guest = user()   // all defaults

    println(alice)   // User(name=Alice, age=30, email=test@example.com, role=admin)
    println(guest)   // User(name=Test User, age=25, email=test@example.com, role=member)
}

Leave a Comment

Your email address will not be published. Required fields are marked *