Skip to content

The Builder design pattern is a creational design pattern that a lot of Java developers are frequently using. It is also one of the 23 Gang of Four design patterns described by Erich Gamma et al. It lets you construct immutable complex objects step by step and produces different types and representations of an object. The Builder is similar to another creational design pattern, the Abstract Factory pattern.

Joshua Bloch, in his book Effective Java, introduced an improved version of the original Builder pattern which focusses only on parts of the original. Bloch's pattern mainly addresses the telescoping constructor problem in Java and uses a fluent design with immutability. In the following article, we will focus only on this variant.

This pattern consists only of two of the four original components: the ConcreteBuilderand the Product. It creates an inner static builder class that can be accessed without creating an instance of the outer product class but still has access to the outer private constructor. If you are using the Lombok project, the @Builder annotation will generate you exactly this, without any manual effort needed.

In this blog post, we want to take a look at how this builder pattern translates to Kotlin and what are the options if you want to use this design pattern in Kotlin.

Too long; didn't read (TLDR) #

  • For most use cases you don't need a Builder pattern in Kotlin and are fine just using a constructor with named arguments and default parameters.
  • In case you have a complex object and/or have a stateful configuration that you want to pass around until you have build the complete object, a Builder is useful.
  • For even complex use cases you can use a type-safe builder to create a custom DSL to ease builder usage

Telescoping Constructor Problem #

One natural way in Java how to deal with optional parameters is by using different constructor argument combinations, because the objects are intended to be immutable, we cannot use setters.

In this article, we will be using the following example class. A Book has two mandatory fields: The ISBN, which refers to a book's 10- or 13-digit International Standard Book Number, and a title. All remaining fields are optional.

public class Book {
private final String isbn;
private final String title;
private final String author;
private final Genre genre;

public Book(String isbn, String title) {
this(isbn, title, null, null);
}

public Book(String isbn, String title, String author) {
this(isbn, title, author, null);
}

public Book(String isbn, String title, String author, Genre genre) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.genre = genre;
}
}

The telescoping constructor approach is used in the example: The first constructor is taking only the mandatory fields. For each optional field, there is an additional constructor that takes the mandatory fields plus the optional ones. Be aware, that the telescoping constructors here don't avoid passing null values in some cases. For validating the input we would need to extend the constructors with argument validation.

The Effective Java Builder Pattern #

Now we are going to take a look how the standard Builder pattern in Java would look like for the Book class:

public class Book {
private final String isbn;
private final String title;
private final String author;
private final Genre genre;

private Book(Builder builder){
this.isbn = builder.isbn;
this.title = builder.title;
this.author = builder.author;
this.genre = builder.genre;
}

public static class Builder {
private final String isbn;
private final String title;
private String author;
private Genre genre;

public Builder(String isbn, String title) {
this.isbn = isbn;
this.title = title;
}

public Builder author(String author){
this.author = author;
return this;
}

public Builder genre(Genre genre){
this.genre = genre;
return this;
}

public Book build(){
return new Book(this);
}
}
}

First we are creating an instance of the Builderclass by passing the mandatory fields to its constructor. Then, we are using fluent setter-like methods of the Builder class to specify optional fields. Once you have specified the desired fields, you call the build method on the Builder instance which finally creates the Book (and where you can add validation if wanted).

Note the following here:

  • The scope of the Book constructor has been changed to private to restrict access to the Builder.
  • The Book constructor takes only the Builder instance as a parameter which contains all the values to be set.
  • The Builder class contains the same fields as the Book as it needs to temporaily hold all the data.
  • The optional fields need to be defined as not final in the Builder to allow optional definition and the setters are returning the Builder instance to allow fluent method chaining.

Implementation in Kotlin #

Kotlin provides many useful features such as named and default parameters, apply() and data class which avoid the need of most of the above code in Kotlin. There are multiple ways how you can approach this, therefore we are at first taking a look at a Java-style implementation and then different shorter Kotlin-style implementations.

Java-Style Kotlin Implementation #

Let's start with plain transforming the above code into Kotlin "Java-style". We have our inner Builder class which has the same fields as the Book. We are using the apply function to support the fluent method chaining approach. Finally, with the build method we are calling the outer constructor to create the book.

class Book private constructor(builder: Builder) {
private val isbn: String
private val title: String
private val author: String?
private val genre: Genre?

init {
isbn = builder.isbn
title = builder.title
author = builder.author
genre = builder.genre
}

class Builder(val isbn: String, val title: String) {
var author: String? = null
private set
var genre: Genre? = null
private set
fun author(author: String)= apply { this.author = author }

fun genre(genre: Genre) = apply { this.genre = genre }

fun build() = Book(this)
}
}

Kotlin-Style with Data Class and Builder #

Kotlin supports named and default parameters that helps to minimize the number of overloads and completely avoids the issue of telescoping constructors for our example. This is a good example, why it is good to revisit best practices for Java developers when starting with Kotlin. Many of the best practices in Java can be replaced with better alternatives in Kotlin. In addition to the default parameters, we are going to take advantage of Kotlin's data class feature to reduce the code even more and we continue to use the apply method for fluent setters.

class Book private constructor(
val isbn: String,
val title: String,
val author: String?,
val genre: Genre?
) {

data class Builder(
val isbn: String,
val title: String,
var author: String? = null,
var genre: Genre? = null
) {
fun author(author: String) = apply { this.author = author }
fun genre(genre: Genre) = apply { this.genre = genre }
fun build() = Book(isbn, title, author, genre)
}
}

Kotlin-Style with only Data Class #

Many Kotlin developers will go even further and say that the Builder pattern used in Java is a way to live without named parameters and that in languages like Kotlin or Python it is good practice to have constructors with long lists of (optional) parameters and if validation is needed, this can be done in the init block.

This opinion is fine and often it is enough, nevertheless, be aware that one of the ideas of the Builder pattern is to delay complex object construction and support stateful configuration of the immutable object. If you need to use something like this, the Builder pattern is still a useful tool that can't be omitted.

If you are fine with just using a data class and have a mixed Java + Kotlin project you can use the @JvmOverloads annotation to let Kotlin generate the telecoping constructors for optional field. You can use the named parameters to explictily state the fields you want to provide. Here is the example using our Book class:

data class Book @JvmOverloads constructor(
val isbn: String,
val title: String,
val author: String? = null,
val genre: Genre? = null
)

val book = Book(
title = "Effective Java",
isbn = "0321356683",
author = "Joshua Bloch"
)

Kotlin-Style with apply() #

Without the use of the Builder class you can use a regular class and a slight variation of the above with using the apply method.

class Book (val isbn: String, val title:String) { 
var author: String? = null
var genre: Genre? = null
}

val book = Book(title = "Effective Java", isbn = "0321356683").apply {
author = "Joshua Bloch"
}

Kotlin supports the idea of passing functions as method parameters (as does Java). This allows us to call lambda functions on the receiver side of the function without any specific qualifiers.

One of the most used, and one of the greatest functions in the Kotlin standard libray in my opinion:

public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

Kotlin-Style with Type-safe Builders #

If you have complex objects that you need to instantiate in a semi-declarative way, you can use the powerful "type-safe builders" in Kotlin and declare extra funs as builders. A good example you can find on the official documentation page. This concept extends the idea of the apply function, presented above, and allows you to create a powerful DSL for building complex objects via nested functions.

Examples of using type-safe builders for providing a DSL are:

Conclusion #

The most important thing when you come from Java is to get familiar with the best practices in Kotlin. In most cases it is fine to simply use a constructor with named arguments and default parameters in Kotlin. Nevertheless, be aware that one of the ideas of the Builder pattern is to delay complex object construction and support stateful configuration of the immutable object. If you need to use something like this, the Builder pattern is still a useful pattern. If you want to have a semi-declarative way you can use the type-safe builders concept of Kotlin to make a custom DSL instead of the classical Java Builder pattern.

Image of the author

Published by Jan Nonnen

Visit author page