| name | Kotlin DSL Patterns |
| user-invocable | false |
| description | Use when domain-specific language design in Kotlin using type-safe builders, infix functions, operator overloading, lambdas with receivers, and patterns for creating expressive, readable DSLs for configuration and domain modeling. |
| allowed-tools | [] |
Kotlin DSL Patterns
Introduction
Kotlin's language features enable creation of expressive domain-specific
languages (DSLs) that feel like natural extensions of the language itself. DSLs
improve code readability, reduce boilerplate, and provide type-safe APIs for
configuration, builders, and domain modeling.
Key features supporting DSL design include lambdas with receivers, extension
functions, infix notation, operator overloading, and scope control. These
features combine to create fluent, intuitive APIs that express domain concepts
clearly without sacrificing type safety or IDE support.
This skill covers type-safe builders, lambda receivers, infix functions,
operator overloading, and practical patterns for designing maintainable DSLs in
Android, testing, and configuration contexts.
Type-Safe Builders
Type-safe builders use lambdas with receivers to create hierarchical structures
with compile-time validation and IDE support.
class HTML {
private val elements = mutableListOf<Element>()
fun head(init: Head.() -> Unit) {
val head = Head()
head.init()
elements.add(head)
}
fun body(init: Body.() -> Unit) {
val body = Body()
body.init()
elements.add(body)
}
override fun toString(): String {
return "<html>\n${elements.joinToString("\n")}\n</html>"
}
}
abstract class Element(val name: String) {
private val children = mutableListOf<Element>()
protected fun <T : Element> initElement(element: T, init: T.() -> Unit): T {
element.init()
children.add(element)
return element
}
override fun toString(): String {
return if (children.isEmpty()) {
"<$name/>"
} else {
"<$name>\n${children.joinToString("\n")}\n</$name>"
}
}
}
class Head : Element("head") {
fun title(text: String) {
initElement(Title()) { this.text = text }
}
}
class Title : Element("title") {
var text: String = ""
override fun toString() = "<title>$text</title>"
}
class Body : Element("body") {
fun h1(text: String) {
initElement(H1()) { this.text = text }
}
fun p(text: String) {
initElement(P()) { this.text = text }
}
fun div(cssClass: String = "", init: Div.() -> Unit) {
initElement(Div(cssClass), init)
}
}
class H1 : Element("h1") {
var text: String = ""
override fun toString() = "<h1>$text</h1>"
}
class P : Element("p") {
var text: String = ""
override fun toString() = "<p>$text</p>"
}
class Div(private val cssClass: String = "") : Element("div") {
fun p(text: String) {
initElement(P()) { this.text = text }
}
override fun toString(): String {
val classAttr = if (cssClass.isNotEmpty()) " class=\"$cssClass\"" else ""
return "<div$classAttr>...</div>"
}
}
fun buildPage() = html {
head {
title("My Page")
}
body {
h1("Welcome")
p("This is a paragraph")
div("container") {
p("Nested paragraph")
}
}
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
class ServerConfig {
var port: Int = 8080
var host: String = "localhost"
val routes = mutableListOf<Route>()
fun route(path: String, init: Route.() -> Unit) {
val route = Route(path)
route.init()
routes.add(route)
}
}
class Route(val path: String) {
var method: String = "GET"
var handler: (Request) -> Response = { Response(200, "OK") }
fun get(handler: (Request) -> Response) {
this.method = "GET"
this.handler = handler
}
fun post(handler: (Request) -> Response) {
this.method = "POST"
this.handler = handler
}
}
data class Request(val path: String, val body: String = "")
data class Response(val status: Int, val body: String)
fun server(init: ServerConfig.() -> Unit): ServerConfig {
val config = ServerConfig()
config.init()
return config
}
val config = server {
port = 9000
host = "0.0.0.0"
route("/api/users") {
get { request ->
Response(200, "User list")
}
}
route("/api/posts") {
post { request ->
Response(201, "Post created")
}
}
}
Type-safe builders provide IDE autocompletion and compile-time validation while
creating readable, hierarchical structures.
Lambdas with Receivers
Lambdas with receivers enable DSL functions to access receiver properties and
methods directly, creating implicit context for cleaner APIs.
fun buildString(action: StringBuilder.() -> Unit): String {
val builder = StringBuilder()
builder.action()
return builder.toString()
}
val result = buildString {
append("Hello")
append(" ")
append("World")
}
class Query {
private val conditions = mutableListOf<String>()
fun where(condition: String) {
conditions.add(condition)
}
fun build(): String {
return "SELECT * WHERE ${conditions.joinToString(" AND ")}"
}
}
fun query(init: Query.() -> Unit): String {
val query = Query()
query.init()
return query.build()
}
val sql = query {
where("age > 18")
where("status = 'active'")
}
class TestSuite(val name: String) {
private tests = mutableListOf<Test>()
{
context = TestContext()
context.block()
tests.add(Test(name, context))
}
{
println()
tests.forEach { it.run() }
}
}
{
assertions = mutableListOf<() -> >()
{
assertions.add {
(expected != ) {
AssertionError()
}
}
}
}
( name: String, context: TestContext) {
{
println()
context.assertions.forEach { it() }
}
}
: TestSuite {
suite = TestSuite(name)
suite.()
suite
}
testSuite = suite() {
test() {
assertEquals(, + )
assertEquals(, - )
}
test() {
assertEquals(, * )
}
}
(
name: String = ,
age: = ,
email: String =
)
= Person().apply {
name =
age =
email =
}
{
with(config) {
println()
routes.forEach { route ->
println()
}
}
}
Lambdas with receivers enable accessing receiver members without explicit
qualifiers, creating natural, context-aware DSL syntax.
Infix Functions and Operators
Infix functions and operator overloading enable natural mathematical and logical
expressions in DSLs, improving readability for domain concepts.
infix fun String.shouldEqual(expected: String) {
if (this != expected) {
throw AssertionError("Expected '$expected' but got '$this'")
}
}
"hello" shouldEqual "hello"
class Duration(val milliseconds: Long) {
operator fun plus(other: Duration) =
Duration(milliseconds + other.milliseconds)
override fun toString() = "${milliseconds}ms"
}
infix fun Int.seconds(unit: Unit) = Duration(this * 1000L)
infix fun Int.minutes(unit: Unit) = Duration(this * 60 * 1000L)
object Unit
val timeout = 5 seconds
interval = minutes
( field: String, : String, value: Any)
= Condition(, , value)
= Condition(, , value)
= Condition(, , value)
{
conditions = mutableListOf<Condition>()
{
conditions.add(condition)
}
: List<Condition> {
listOf(, other)
}
: String {
}
}
: String {
builder = QueryBuilder()
builder.()
builder.build()
}
query1 = queryBuilder {
( gt )
( eq )
}
( x: , y: ) {
=
Vector(x + other.x, y + other.y)
=
Vector(x * scalar, y * scalar)
=
Vector(-x, -y)
}
v1 = Vector(, )
v2 = Vector(, )
v3 = v1 + v2
v4 = v1 *
v5 = -v1
{
routes = mutableMapOf<String, (Request) -> Response>()
{
routes[path] = handler
}
: Response {
routes[request.path]?.invoke(request)
?: Response(, )
}
}
router = Router()
router() { request ->
Response(, )
}
{
values = mutableMapOf<String, Any>()
: Any? = values[key]
{
values[key] = value
}
}
config1 = Configuration()
config1[] =
timeout1 = config1[]
Infix functions remove parentheses and dots for binary operations, while
operator overloading enables natural mathematical notation in DSLs.
Scope Control with @DslMarker
DslMarker annotations prevent implicit receivers from outer scopes, improving
DSL safety by catching accidental nesting errors at compile time.
class Table(val name: String) {
val columns = mutableListOf<Column>()
fun column(name: String, init: Column.() -> Unit) {
val column = Column(name)
column.init()
columns.add(column)
}
}
class Column(val name: String) {
var type: String = "VARCHAR"
var nullable: Boolean = true
fun column(name: String, init: Column.() -> Unit) {
}
}
fun problematicDSL() = Table("users") {
column("id") {
type = "INT"
column("nested") {
type = "TEXT"
}
}
}
@DslMarker
annotation class DatabaseDsl
@DatabaseDsl
class ( name: String) {
columns = mutableListOf<SafeColumn>()
{
column = SafeColumn(name)
column.()
columns.add(column)
}
}
( name: String) {
type: String =
nullable: =
}
= SafeTable() {
column() {
type =
}
}
{
{
SafeBody().()
}
}
{
{}
}
{
{
SafeTestContext().block()
}
}
{
{}
}
{
SafeHTML {
body {
}
}
SafeTestSuite {
test() {
}
}
}
DslMarker prevents confusing implicit receiver access across DSL boundaries,
making DSLs safer and more maintainable.
Gradle Kotlin DSL Patterns
Gradle's Kotlin DSL demonstrates real-world DSL patterns for build
configuration, dependency management, and task definition.
plugins {
kotlin("jvm") version "1.9.0"
id("application")
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
}
abstract class CustomTask : DefaultTask() {
@get:Input
abstract val message: Property<String>
@TaskAction
fun execute() {
println(message.get())
}
}
tasks {
register<CustomTask>("greet") {
message.set("Hello from custom task")
}
}
fun Project.configureKotlin() {
kotlin {
jvmToolchain(17)
}
}
fun Project.configureTesting() {
tasks.withType<Test> {
useJUnitPlatform()
}
}
abstract class MyPluginExtension {
abstract val version: Property<String>
abstract val enabled: Property<Boolean>
init {
version.convention("1.0.0")
enabled.convention(true)
}
}
: <> {
{
extension = project.extensions.create(
,
MyPluginExtension::.java
)
project.tasks.register() {
doLast {
println()
println()
}
}
}
}
configure<MyPluginExtension> {
version.()
enabled.()
}
compileKotlin: KotlinCompile tasks
compileKotlin.kotlinOptions {
jvmTarget =
freeCompilerArgs = listOf()
}
kotlin {
jvm {
withJava()
}
js(IR) {
browser()
nodejs()
}
sourceSets {
commonMain getting {
dependencies {
implementation()
}
}
commonTest getting {
dependencies {
implementation(kotlin())
}
}
}
}
Gradle Kotlin DSL combines multiple DSL patterns to create expressive,
type-safe build configuration with excellent IDE support.
Ktor DSL for Web Applications
Ktor demonstrates DSL patterns for routing, serialization, and server
configuration in web applications.
fun Application.module() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
})
}
routing {
get("/") {
call.respondText("Hello, world!")
}
get("/users/{id}") {
val id = call.parameters["id"]
call.respond(User(id?.toInt() ?: 0, "User $id"))
}
post("/users") {
val user = call.receive<User>()
call.respond(HttpStatusCode.Created, user)
}
route("/api") {
get("/health") {
call.respondText("OK")
}
authenticate("auth-jwt") {
get("/protected") {
call.respondText("Protected route")
}
}
}
}
}
fun Route.userRoutes() {
route("/users") {
get {
call.respond(listOf(User(1, "Alice"), User(2, "Bob")))
}
get("/{id}") {
val id = call.parameters["id"]?.toInt() ?: return@get call.respond(
HttpStatusCode.BadRequest
)
call.respond(User(id, ))
}
}
}
->
) {
(path) {
params = call.receive<T>()
handler(params)
}
}
( name: String, minAge: )
{
typedGet<UserQuery>() { query ->
call.respondText()
}
}
Ktor's DSL provides readable, declarative server configuration while maintaining
type safety and composability.
Best Practices
-
Use @DslMarker to prevent scope confusion by restricting implicit
receivers and catching incorrect nesting at compile time
-
Keep DSL scope focused on a single domain to maintain clarity and prevent
mixing unrelated concepts in one DSL
-
Provide sensible defaults in DSL builders to reduce boilerplate while
allowing customization when needed
-
Leverage lambdas with receivers for context-aware DSL syntax that enables
direct member access without qualifiers
-
Use infix functions sparingly only for natural binary operations like
comparisons, logical operators, or domain relationships
-
Document DSL usage with examples to show intended patterns and prevent
misuse of flexible DSL APIs
-
Validate DSL structures at build time rather than runtime to catch errors
early with clear compilation errors
-
Make DSLs immutable when possible to prevent unexpected modifications and
enable safer concurrent usage
-
Provide both DSL and non-DSL APIs to give users choice between
expressiveness and explicitness
-
Test DSL usage patterns extensively to ensure intuitive behavior and
catch edge cases in complex nesting scenarios
Common Pitfalls
-
Overusing infix functions for inappropriate operations makes code harder
to read and understand without clear conventions
-
Creating overly complex DSLs that try to do too much leads to confusing
APIs that are hard to learn and maintain
-
Forgetting @DslMarker annotation allows implicit receivers from outer
scopes, causing subtle bugs in nested DSL structures
-
Not validating DSL structures allows invalid configurations to pass
compilation and fail at runtime
-
Using mutable DSL builders without protection enables unsafe concurrent
modifications and unexpected behavior
-
Creating deeply nested DSLs without clear structure becomes difficult to
navigate and reason about
-
Overloading too many operators obscures intent and makes code cryptic
rather than expressive
-
Mixing DSL and imperative code inconsistently creates confusion about
which style to use where
-
Not providing clear DSL boundaries makes it unclear when DSL context
starts and ends in code
-
Ignoring IDE support implications creates DSLs that don't work well with
autocompletion or refactoring tools
When to Use This Skill
Use Kotlin DSL patterns when building libraries, frameworks, or configuration
systems that benefit from readable, type-safe domain-specific syntax.
Apply type-safe builders for hierarchical structures like HTML generation, UI
layouts, or nested configuration trees where structure matters.
Employ infix functions and operators for domain concepts that have natural
binary operations like comparisons, measurements, or logical relationships.
Leverage lambdas with receivers for test frameworks, build scripts, or any API
where context-specific operations improve readability.
Use @DslMarker when building complex nested DSLs to prevent scope confusion and
catch errors at compile time rather than runtime.
Resources