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)
}