Skip to Content
Course content

123: The Builder Pattern with Kotlin DSLs

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've spent any time in the Java ecosystem, you're probably intimately familiar with the Builder pattern. It was the standard answer for the "telescoping constructor" problem—where you end up with five different constructors just to handle various combinations of optional parameters. In Java, you'd write a separate Builder class, chain a bunch of .setX().setY() methods, and finally call .build(). It works, but in Kotlin, it feels like we're bringing a knife to a gunfight.

The Java Hangover: Chained Setters

Let's look at how we'd build a complex HttpRequest object the "old" way. I'll show you the pattern I see most often from developers who are still thinking in Java terms. It's not technically "wrong," but it's noisy.

class HttpRequest private constructor(
    val url: String,
    val method: String,
    val headers: Map<String, String>,
    val body: String?
) {
    class Builder {
        private var url: String = ""
        private var method: String = "GET"
        private val headers = mutableMapOf<String, String>()
        private var body: String? = null

        fun setUrl(url: String) = apply { this.url = url }
        fun setMethod(method: String) = apply { this.method = method }
        fun addHeader(key: String, value: String) = apply { headers[key] = value }
        fun setBody(body: String) = apply { this.body = body }

        fun build() = HttpRequest(url, method, method, headers, body)
    }
}

// Usage
val request = HttpRequest.Builder()
    .setUrl("https://api.example.com/v1")
    .setMethod("POST")
    .addHeader("Content-Type", "application/json")
    .setBody("{ \"id\": 123 }")
    .build()

The problem here isn't the logic; it's the ceremony. You're constantly repeating the builder's context, and the syntax is purely imperative. It reads like a list of instructions rather than a definition of a request. Plus, you're stuck with a mutable builder object hanging around until the final build() call.

Levelling Up to a Type-Safe DSL

Kotlin allows us to turn this into a Domain Specific Language (DSL) by leveraging function literals with receivers. Instead of chaining methods, we can create a block of code where this refers to the builder itself. This transforms the experience from "calling methods on an object" to "configuring an object within a scope."

I prefer this approach because it visually separates the configuration from the execution. Here is how I would refactor that same request logic:

class HttpRequest(
    val url: String,
    val method: String,
    val headers: Map<String, String>,
    val body: String?
)

class HttpRequestBuilder {
    var url: String = ""
    var method: String = "GET"
    private val headers = mutableMapOf<String, String>()
    var body: String? = null

    fun header(key: String, value: String) {
        headers[key] = value
    }

    fun build() = HttpRequest(url, method, headers, body)
}

// This is the magic part: the DSL entry point
fun httpRequest(block: HttpRequestBuilder.() -> Unit): HttpRequest {
    return HttpRequestBuilder().apply(block).build()
}

// Usage
val request = httpRequest {
    url = "https://api.example.com/v1"
    method = "POST"
    header("Content-Type", "application/json")
    body = "{ \"id\": 123 }"
}

Notice the difference? We've stripped away the .set... prefixes. By using HttpRequestBuilder.() -> Unit, we've told Kotlin that the lambda provided to httpRequest should be executed inside the context of an HttpRequestBuilder instance. It's cleaner, it's more declarative, and it feels native to the language.

When the Magic Becomes a Liability

Now, a word of caution. DSLs are powerful, but they can leak. In the example above, if HttpRequestBuilder had a build() method that was public, a user could technically call build() inside the httpRequest { ... } block, which would be completely nonsensical.

If you're building a complex, nested DSL—say, for a UI layout or a HTML generator—you'll find that the scope can get messy. You might accidentally call a method from an outer builder while you're inside an inner builder. To prevent this, I highly recommend using the @DslMarker annotation. It tells the compiler to forbid calling members of outer receiver scopes from within inner ones. It's a small addition that saves you from hours of debugging "Why is this value being set in the wrong object?"

Essentially, the trade-off is simple: you're exchanging the rigid, explicit nature of the Java Builder for a more flexible, concise syntax. For most internal configuration objects in a Kotlin project, the DSL approach is the clear winner.




📋 Practical Task

Build a Type-Safe Email Composition DSL

You need to create a system for constructing emails. An Email should have a recipient, a subject, a body, and an optional list of attachments. Instead of using a standard constructor, implement this using a Kotlin DSL.

Requirements:

  • Create an Email data class to hold the final values.
  • Create an EmailBuilder class that handles the mutable state during construction.
  • Implement a top-level email { ... } function that initializes the builder, applies the provided lambda, and returns a completed Email object.
  • The DSL should allow adding multiple attachments using a function (e.g., attachment("file.pdf")) rather than adding to a list directly.

Verification: Your code should allow you to instantiate an email like this:

val myEmail = email {
    recipient = "dev@example.com"
    subject = "Project Update"
    body = "The build is finally green!"
    attachment("logs.txt")
    attachment("screenshot.png")
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.