404 Not Found

404 Not Found


nginx

Kotlin DSL Building Explained

DSL is Kotlin's most expressive feature — it reads like a configuration file but is actually type-safe Kotlin code. Charlie uses it to build an order configuration DSL, allowing non-technical users to define order rules.

1. What You'll Learn


2. A Real Architect's Story

(1) Pain Point: Unsafe Configuration Formats

Charlie's order rules used YAML configuration, but YAML has no type checking — typos and missing fields only surfaced at runtime. This caused 2-3 production incidents per month from configuration errors.

(2) Kotlin DSL's Solution

KOTLIN
// YAML: no type safety, errors at runtime
order:
  customer: Alice
  items:
    - sku: ABC
      qty: three  // TYPO! Should be 3

// Kotlin DSL: type-safe, errors at compile time
order {
    customer = "Alice"
    items {
        item { sku = "ABC"; qty = 3 }  // Compile-time type check!
    }
}

Kotlin DSL = flexibility of configuration + type safety of code. The compiler catches configuration errors at compile time.


3. Lambda Receivers

(1) Lambdas with Receiver

KOTLIN
// Regular lambda: parameter
val greet: (String) -> Unit = { name -> println("Hello, $name") }

// Lambda with receiver: 'this' refers to receiver
val greet2: String.() -> Unit = { println("Hello, $this") }

// Usage
greet2("Alice")          // Hello, Alice
"Alice".greet2()         // Hello, Alice (extension-like call)

(2) with / apply / run

KOTLIN
val order = Order()

// with: execute block with receiver, return block result
val summary = with(order) {
    id = "ORD-001"
    total = 299.99
    "Order $id created"  // Return value
}

// apply: execute block with receiver, return receiver itself
val configured = order.apply {
    id = "ORD-001"       // 'this' is order
    total = 299.99
}  // Returns order

// run: execute block with receiver, return block result
val result = order.run {
    id = "ORD-002"
    "Processed $id"
}

(3) Scope Function Comparison

Function Reference Return Value Use Case
apply this Receiver Object configuration (chaining)
run this Block result Execute + return result
with this Block result Non-extension usage
let it Block result Null-safety transformations
also it Receiver Side effects (logging / validation)

4. Type-Safe Builders

(1) Basic Builder

KOTLIN
class OrderBuilder {
    var id: String = ""
    var total: Double = 0.0
    var status: String = "PENDING"
    var customer: String = ""
    private val items = mutableListOf<OrderItem>()

    fun item(block: OrderItemBuilder.() -> Unit) {
        items.add(OrderItemBuilder().apply(block).build())
    }

    fun build() = Order(id, total, status, customer, items.toList())
}

class OrderItemBuilder {
    var sku: String = ""
    var qty: Int = 1
    var unitPrice: Double = 0.0
    fun build() = OrderItem(sku, qty, unitPrice)
}

// DSL entry point
fun order(block: OrderBuilder.() -> Unit): Order {
    return OrderBuilder().apply(block).build()
}

(2) Using the Builder DSL

KOTLIN
val myOrder = order {
    id = "ORD-001"
    total = 299.99
    customer = "Alice"
    item {
        sku = "SKU-WIDGET"
        qty = 3
        unitPrice = 9.99
    }
    item {
        sku = "SKU-GADGET"
        qty = 1
        unitPrice = 149.99
    }
}

5. @DslMarker to Prevent Scope Leakage

(1) Problem: Implicit Receiver Ambiguity

KOTLIN
order {
    id = "ORD-001"
    item {
        sku = "ABC"
        // DANGER: 'id' here could refer to outer OrderBuilder.id!
        id = "ITEM-001"  // Which id? Order or Item?
    }
}

(2) @DslMarker Solution

KOTLIN
@DslMarker
annotation class OrderDsl

@OrderDsl
class OrderBuilder {
    var id: String = ""
    fun item(block: OrderItemBuilder.() -> Unit) { ... }
}

@OrderDsl
class OrderItemBuilder {
    var sku: String = ""
    // Now: cannot access outer OrderBuilder.id from here!
}

order {
    id = "ORD-001"  // OK: OrderBuilder.id
    item {
        sku = "ABC" // OK: OrderItemBuilder.sku
        // id = "X"  // ERROR: cannot access outer scope!
    }
}

(3) DSL Build Flow

100%
flowchart TD
    A[order block] --> B[OrderBuilder<br/>id, total, customer]
    B --> C[item block]
    C --> D[OrderItemBuilder<br/>sku, qty, price]
    D --> E[build OrderItem]
    E --> F[add to items list]
    B --> G[build Order]
    G --> H[Return Order object]

6. DSL Design Pattern Comparison

Pattern Implementation Readability Type Safety Use Case
Builder + Lambda Receiver fun order(block: Builder.() -> Unit) ★★★★★ ✅ Compile-time Complex nested configuration
Infix Functions infix fun A.to(b: B) ★★★★ ✅ Compile-time Simple chaining
Operator Overloading operator fun plus() ★★★ ✅ Compile-time Math / collection ops
Annotation Processing @Annotation class X ★★★ ✅ Generation-time Reducing boilerplate
Dynamic Proxy dynamic style ★★ ❌ Runtime Flexible config / scripts

7. Gradle Kotlin DSL Example

One of Kotlin DSL's most successful applications — Gradle build scripts:

KOTLIN
// build.gradle.kts - this IS a Kotlin DSL!
plugins {
    kotlin("jvm") version "1.9.22"
    application
}

dependencies {
    implementation(kotlin("stdlib"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    testImplementation(kotlin("test"))
}

kotlin {
    jvmToolchain(17)
}

application {
    mainClass.set("com.order.MainKt")
}

7. Complete Example: OrderProcessor Order Configuration DSL

KOTLIN
// ============================================
// OrderProcessor - Order Configuration DSL
// Feature: Type-safe order builder DSL
// ============================================

@DslMarker
annotation class OrderDsl

data class OrderItem(val sku: String, val qty: Int, val unitPrice: Double) {
    val subtotal: Double get() = qty * unitPrice
}

data class Address(val street: String, val city: String, val country: String)

data class Order(
    val id: String,
    val customer: String,
    val items: List<OrderItem>,
    val shippingAddress: Address?,
    val priority: String,
    val notes: String?
) {
    val total: Double get() = items.sumOf { it.subtotal }
}

@OrderDsl
class OrderBuilder {
    var id: String = ""
    var customer: String = ""
    var priority: String = "STANDARD"
    var notes: String? = null
    private val items = mutableListOf<OrderItem>()
    private var shippingAddress: Address? = null

    fun item(block: OrderItemBuilder.() -> Unit) {
        items.add(OrderItemBuilder().apply(block).build())
    }

    fun items(block: ItemsBuilder.() -> Unit) {
        items.addAll(ItemsBuilder().apply(block).build())
    }

    fun shipTo(block: AddressBuilder.() -> Unit) {
        shippingAddress = AddressBuilder().apply(block).build()
    }

    fun build(): Order {
        require(id.isNotBlank()) { "Order ID is required" }
        require(customer.isNotBlank()) { "Customer is required" }
        return Order(id, customer, items.toList(), shippingAddress, priority, notes)
    }
}

@OrderDsl
class OrderItemBuilder {
    var sku: String = ""
    var qty: Int = 1
    var unitPrice: Double = 0.0

    fun build(): OrderItem {
        require(sku.isNotBlank()) { "SKU is required" }
        return OrderItem(sku, qty, unitPrice)
    }
}

@OrderDsl
class ItemsBuilder {
    private val items = mutableListOf<OrderItem>()
    fun item(block: OrderItemBuilder.() -> Unit) {
        items.add(OrderItemBuilder().apply(block).build())
    }
    fun build() = items.toList()
}

@OrderDsl
class AddressBuilder {
    var street: String = ""
    var city: String = ""
    var country: String = ""
    fun build() = Address(street, city, country)
}

// DSL entry point
fun order(block: OrderBuilder.() -> Unit): Order =
    OrderBuilder().apply(block).build()

fun main() {
    // Type-safe DSL: reads like configuration, verified by compiler
    val myOrder = order {
        id = "ORD-001"
        customer = "Alice"
        priority = "HIGH"

        items {
            item { sku = "SKU-WIDGET"; qty = 3; unitPrice = 9.99 }
            item { sku = "SKU-GADGET"; qty = 1; unitPrice = 149.99 }
            item { sku = "SKU-DOOHICKEY"; qty = 5; unitPrice = 4.50 }
        }

        shipTo {
            street = "123 Main St"
            city = "New York"
            country = "US"
        }

        notes = "Rush delivery requested"
    }

    println("=== Order Summary ===")
    println("ID: ${myOrder.id}")
    println("Customer: ${myOrder.customer}")
    println("Priority: ${myOrder.priority}")
    println("\nItems:")
    myOrder.items.forEach { item ->
        println("  ${item.sku} x${item.qty} @ \$${item.unitPrice} = \$${item.subtotal} USD")
    }
    println("\nTotal: \$${myOrder.total} USD")
    println("Ship to: ${myOrder.shippingAddress}")
    println("Notes: ${myOrder.notes}")

    // Validation: compile-time type safety
    // sku = 123       // ERROR: String expected, not Int
    // qty = "three"   // ERROR: Int expected, not String
}

Output:

TEXT
=== Order Summary ===
ID: ORD-001
Customer: Alice
Priority: HIGH

Items:
  SKU-WIDGET x3 @ $9.99 = $29.97 USD
  SKU-GADGET x1 @ $149.99 = $149.99 USD
  SKU-DOOHICKEY x5 @ $4.5 = $22.5 USD

Total: $202.46 USD
Ship to: Address(street=123 Main St, city=New York, country=US)
Notes: Rush delivery requested

❓ FAQ

Q What's the difference between a DSL and a regular API?
A A DSL aims to "read like natural language or configuration", achieved through lambda receivers + extension functions + infix calls. Regular APIs prioritize functionality over readability.
Q Is @DslMarker mandatory?
A Not mandatory, but strongly recommended. Without it, nested DSL scopes can accidentally access members from outer scopes, leading to bugs. @DslMarker prevents this at compile time.
Q What's the difference between lambda receivers and lambda parameters?
A Receivers are accessed via this (implicit); parameters via it (explicit). Receivers suit DSLs (implicit this feels more natural); parameters suit callbacks (explicit it is clearer).
Q What's the performance of DSLs?
A DSLs create Builder objects, which incur slight allocation overhead. But in configuration scenarios (non-hot-paths), this is negligible. Avoid using DSLs in inner loops with millions of calls per second.
Q What's the difference between Kotlin DSL and Groovy DSL?
A Kotlin DSL has compile-time type checking, IDE autocompletion, and refactoring support. Groovy DSL is dynamically flexible but lacks compile-time checks. Gradle Kotlin DSL is gradually replacing Groovy DSL.
Q How do I add conditional logic to a DSL?
A The DSL is itself Kotlin code — you can use if/when/for and other control flow inside blocks. This is a major advantage over configuration files — configuration + logic in one.

📖 Summary


📝 Exercises

  1. Beginner (⭐): Use apply to configure an Order object, setting id, total, and status. Hint: order.apply { id = "ORD-001"; total = 299.99 }
  2. Intermediate (⭐⭐): Build an EmailBuilder DSL: email { from("a@b.com"); to("c@d.com"); subject("Hi") }. Hint: fun email(block: EmailBuilder.() -> Unit) + lambda receiver
  3. Challenge (⭐⭐⭐): Use @DslMarker to implement a nested DSL: order { customer { name("Alice") }; item { sku("ABC") } }, ensuring inner scopes cannot access outer members. Hint: @DslMarker + multi-layer Builder

← Previous | Next →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏