Blog

Domain-Specific Languages in Kotlin: the Type-Safe Builder pattern

26 Sep, 2022
Xebia Background Header Wave

Domain-specific languages are one of Kotlin’s superpowers. If you build them right, you can offer users a fluent and elegant syntax that is readable and contains less boilerplate. Domain-specific languages, or DSLs, are mini programming languages within your Kotlin codebase that focus on specific concerns. Everything that a DSL can do is also possible using general, ‘non-specific’ language features, such as methods and lambdas. But these mini languages can be cleaner and easier to read. Let’s look at two examples from Spring Boot where domain-specific languages shine.

The first example is the Bean Definition DSL which registers beans using lambdas instead of annotations. An example from the Spring documentation:

val myBeans = beans {
    bean<Foo>()
    bean<Bar>()
    bean("bazBean") {
        Baz().apply {
            message = "Hello world"
        }
    }
    profile("foobar") {
        bean { FooBar(ref("bazBean")) }
    }
    bean(::myRouter)
}

This approach is more efficient as it doesn’t use reflection or proxies. I also really like that you can use if expressions and other constructs. After all, it’s just Kotlin!

Another example is the Router DSL offered by Spring which allows you to define the routes in our application. Have a look at one of my own creations:

fun apiRouter() = coRouter {
    "/api/cats".nest {
        accept(APPLICATION_JSON).nest {
            GET("", catHandler::getAll)


            contentType(APPLICATION_JSON).nest {
                POST("", catHandler::add)
            }


            "/{id}".nest {
                GET("", catHandler::getById)
                DELETE("", catHandler::delete)


                contentType(APPLICATION_JSON).nest {
                    PUT("", catHandler::update)
                }
            }
        }
    }
}

All words in this DSL, except nest, tell you something about how we configured the routes: a sign of low boilerplate. The Router DSL uses nesting to let you group routes based on shared properties, such as path segments or content types.

The nested nature of DSLs

This nested structure is the core characteristic of domain-specific languages in Kotlin. When using a DSL, we are actually constructing a complex, hierarchical object. A written DSL should give you immediate insight into what the resulting object will look like. To see this, have a look at this domain-specific language for creating HTML documents and the object it constructs:

The DSL’s structure resembles the object you construct

The DSL consists of builder constructs that describe what an object will look like. Builder constructs in our HTML DSL are html, body, head, h1 and p, and within their curly brackets, we define what we want these objects to be like. In the Kotlin world, we call them type-safe builders. Type-safe because all functions and values have types, and so the compiler can validate your writings. This is helpful because you will get an error if you forget something, give it an invalid value, or just make a typo. If the compiler says you’re good, the result should be valid, given that your DSL is correct.

These type-safe builders can look quite magical when you first look at them. Just some word, followed by curly brackets within which the DSL continues.

We can use domain-specific languages for many purposes, from routing configuration to creating HTML. The pattern is always the same, though. Type-safe builders always follow three steps:

  1. Create a new instance of the type you want to build.
  2. Initialize the object instance based on what is written between the curly brackets.
  3. Return the initialized instance (if root builder) or add the instance to the parent builder (if child builder).

The third point might sound confusing at first. In the HTML example above, the head and body builders are nested child builders within the html builder which is the root. Instead of returning, these child builders should contribute their built instances to the parent html builder to construct the overall HTML instance.

The html builder. How do we implement this?

Implementing type-safe builders

So we have now seen the pattern, but how do we implement this? The first step is to define the model of our domain. Let’s assume we want to implement the domain-specific language for HTML we saw above. In this case, we need to model HTML tags. Below we see a possible model.

Domain model: HTML tags

Besides the <html>, <head> and <body> tags, we also support <h1> and <p>. Notice that the latter two have a different base class BodyElement, as we can only use these tags inside the body.

Our first attempt to create the html builder is to define a function that takes a lambda and returns an Html instance. The function simply returns the instance supplied by the lambda. In Kotlin, if the lambda is a function’s last parameter, we can omit the parentheses. That gives us the following syntax:

The above looks quite like the builder. All three steps of the type-safe builder pattern (create, initialize, return) are performed within the curly brackets. However, it does not make sense to ask the user of our DSL to create and return the instance in the builder manually. We want to facilitate that. The first step is to tweak the lambda parameter to have a receiver:

This syntax might look confusing at first sight. I like to think of it as passing an extension function as a parameter. In the scope of this function, the HTML class now has an additional method called init, which the function’s caller defines. If our cursor is within the function and we ask our IDE for all methods on the HTML class, it shows us the class’ new superpower:

This ‘function literal with receiver’ allows us to create the initial HTML instance in the html function, and the user of our DSL can access the instance within the lambda through the `this` keyword. So, our builder now facilitates step 1 of the pattern.

To take care of the third step as well, we need to tweak our lambda parameter again slightly. This time the return type is changed to Unit, which means the lambda will return nothing.

And that’s really all we need to do. The lambda modifies the state of the HTML object, while our builder creates and returns the instance.

Adding the child builders

So that’s how we build the root builder, but of course, we also want to have nested builders. How to do that?

We repeat the same pattern we saw before, but now these functions are methods of the parent builder class. Also, because we deal with child builders, the third step adds the built instance to the parent instead of returning it.

With these methods, we can now have our DSL fully working:

Next steps

We have seen in this article how the type-safe builder pattern works and how we can implement it. These builders are the backbone of every DSL, and you can now build your own. Yet, this is just the start of your journey. The life of a DSL builder starts between the curly brackets, and my next article will show my favourite patterns to create a beautiful DSL. Stay tuned!

Bjorn van der Laan
Software engineer at Xebia Software Development
Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts