Skip to Content
Course content

242: Practice Exercise: Building a Kotlin-Based Configuration DSL

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

Think about ordering a custom pizza. You don't walk into the kitchen and start rearranging the oven temperature or manually placing pepperoni slices on a dough ball. Instead, you use a menu—a highly structured way of communicating your desires to the staff. You say, "I want a large pizza, thin crust, with mushrooms and extra cheese." The menu defines the vocabulary of what's possible, and your order is the configuration.

Building a DSL (Domain Specific Language) in Kotlin is exactly like designing that menu. You aren't writing the logic that bakes the pizza; you're creating a readable, constrained way for someone else to describe how they want the "pizza" (or in our case, a software component) to be built. Here is how that maps to the code we're about to write:

  • The Menu: These are your configuration classes and the "builder" functions. They define what options are available.
  • The Order: This is the lambda block where the user actually sets the values.
  • The Chef: This is the internal logic that takes the finished configuration object and actually initializes the system.

The Power of the Function Literal with Receiver

The secret sauce here is T.() -> Unit. You've seen lambdas before, but when we add that T. prefix, we're telling Kotlin: "Inside this block of code, the keyword this refers to an instance of T." This is what allows us to call methods or set properties of a class without having to explicitly reference an object name over and over. It turns a standard function call into a declarative block.

Structuring Your Configuration Objects

Let's say we're building a DSL to configure a web server. We don't want a flat list of twenty variables; that's messy. We want nesting. I usually start by defining simple data classes or "Config" classes that hold the state.

class SecurityConfig {
    var sslEnabled: Boolean = false
    var certPath: String = ""
}

class ServerConfig {
    var port: Int = 80
    var host: String = "localhost"
    var security = SecurityConfig()

    // This is the nested builder
    fun security(block: SecurityConfig.() -> Unit) {
        security.apply(block)
    }
}

Notice the security function. It takes a lambda with SecurityConfig as the receiver. By calling security.apply(block), we're essentially saying, "Take this existing security object and let the user run their configuration code against it."

Creating the Entry Point

Now, we need a way to kick the whole thing off. We don't want the user to have to manually instantiate ServerConfig and call a build method. We want it to feel like a language. I'll create a top-level function that handles the instantiation and returns the final result.

fun server(block: ServerConfig.() -> Unit): ServerConfig {
    val config = ServerConfig()
    config.apply(block)
    return config
}

// Now, the "user" of our DSL does this:
val myServer = server {
    port = 8080
    host = "production.myapp.com"
    security {
        sslEnabled = true
        certPath = "/etc/ssl/certs/server.crt"
    }
}

I love this pattern because it's type-safe. If you try to set port = "eighty", the compiler will scream at you. You get all the power of a full programming language but the readability of a JSON or YAML file. One quick tip: if your DSL gets complex, look into @DslMarker. It prevents you from accidentally calling a "parent" builder inside a "child" builder, which can lead to some really confusing configuration bugs.




📋 Practical Task

Exercise: Building a Database Connection Pool DSL

Your task is to create a DSL for configuring a database connection pool. A developer using your DSL should be able to define the database URL, the maximum number of connections, and a nested block for "Timeout Settings".

Requirements:

  • Create a TimeoutConfig class with properties for connectionTimeout and idleTimeout (both Integers).
  • Create a DatabaseConfig class with properties for url (String), maxConnections (Int), and a TimeoutConfig object.
  • Implement a timeout { ... } function inside DatabaseConfig that allows for nested configuration of the timeout settings.
  • Create a top-level database { ... } function that initializes the DatabaseConfig and returns it.
  • In your main function, use your DSL to create a configuration with a URL of "jdbc:mysql://localhost:3306/mydb", maxConnections set to 20, a connectionTimeout of 5000, and an idleTimeout of 10000.
  • Print the resulting configuration object to the console to verify the values were set correctly.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.