Type-Safe DSLs
Prerequisites: Scope functions, Extension functions
A DSL (domain-specific language) is an API designed to read like a description rather than like code. You've already used Kotlin DSLs: Gradle build scripts, Ktor routing, and Jetpack Compose are all built from ordinary Kotlin features.
The key ingredient: lambdas with receivers
A function type like StringBuilder.() -> Unit is a lambda with a
receiver: inside it, this is a StringBuilder, so its methods can
be called without a prefix. The standard buildString uses one:
fun main() {
val text = buildString {
append("Kotlin")
append(" ")
append("DSLs")
}
println(text)
}
Kotlin DSLs
Building your own
@DslMarker
annotation class MenuDsl
@MenuDsl
class Menu(val title: String) {
private val items = mutableListOf<Pair<String, Double>>()
fun item(name: String, price: Double) {
items += name to price
}
override fun toString() = buildString {
appendLine("== $title ==")
items.forEach { (name, price) -> appendLine("${name.padEnd(12)}$${"%.2f".format(price)}") }
}.trimEnd()
}
fun menu(title: String, build: Menu.() -> Unit): Menu = Menu(title).apply(build)
fun main() {
val breakfast = menu("Breakfast") {
item("Espresso", 3.0)
item("Croissant", 4.5)
}
println(breakfast)
}
== Breakfast ==
Espresso $3.00
Croissant $4.50
menu(title, build: Menu.() -> Unit)takes a lambda with aMenureceiver, soitem(...)inside the braces meansthis.item(...).Menu(title).apply(build)creates the menu, runs the lambda on it, and returns it.@DslMarkerstops nested blocks from accidentally calling methods of an outer receiver — important once a DSL has several levels, like HTML inside HTML.
When to write one
DSLs shine for configuration, builders, and structured documents — code that's mostly data. For ordinary logic, regular functions and classes are clearer and easier to debug.