Fluent Builders in Java

dastrobu
5 min readMay 16, 2021

--

Most Java developers most likely have at least used a builder and probably implemented some themselves. I find implementing a builder a bit cumbersome since it involves quite some boilerplate code. On the other hand, constructing an object from a builder is usually much more readable than either having a constructor with many parameters or having quite some setter usages.

Classic Builder

This section can be skipped if familiar with the classic builder pattern.

Let’s briefly recap the classic builder pattern in Java. Given a record with some properties, e.g. representing an email, is implemented by

public record Email(String to, String subject, String body) {
}

Java records keep code snippets short. The idea of (fluent) builders is completely unrelated, though, and can be implemented for class-based POJOs in the same way.

Creating a new email object without a builder would be done in the following way

new Email("john.smith@example.com", "New article on builders", "Hi,\ndid you read the new article about fluent builders?")

As people have noticed, it is easy to mix up the order of arguments, and it can easily happen that one tries to send a message with subject john.smith@example.com to the recipient New article on builders. The compiler can’t help much in this case, since it is quite common to have many Strings or ints in real-world applications. Modern IDEs try to help by showing the parameter names, but one can easily overlook these mistakes.

To improve the situation, a classic builder is implemented as follows.

public record Email(String to, String subject, String body) {

public static class Builder {
private String to;
private String subject;
private String body;

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

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

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

public Email build() {
return new Email(to, subject, body);
}
}
}

A new Email instance would then be created by

new Email.Builder()
.withTo("john.smith@example.com")
.withSubject("New article on builders")
.withBody("...")
.build();

This is a lot more readable, especially when there are more properties than just three. It also reduces the risk of mixing up arguments. So it is less likely to send something to a bad recipient New article on builders by accident.

Let’s quickly sum up why there is so much boilerplate code.

  1. Each field in the record is implicitly final (as in any good, immutable POJO). Hence, all fields need to be duplicated into the builder class in a non-final version.
  2. Each with* method must be implemented. Even if many records and builders are having e.g. a subject property, it is hard to reuse code.

There is also a drawback. One can easily forget initializing some properties, leaving some properties null. Although the compiler cannot help here, it is easy to find such issues at runtime if adding some null checks to the constructor.

public record Email(String to, String subject, String body) {
public Email {
Objects.requireNonNull(to);
}
// ...
}

Fluent Builder

After reading Introduction to the Fluent Builder Pattern I started experimenting with this pattern.

First, let’s sum up what a fluent builder should do.

  1. Define a builder with a fluent API.
  2. Reduce boilerplate code.
  3. Reduce the risk of uninitialized fields.

So in essence one should be able to write

new Email.Builder()
.withTo("john.smith@example.com")
.withSubject("New article on builders")
.withBody("...")
.build();

without having to implement the entire builder on every record.

To achieve this, let's define some traits (or at least what comes closest to them in Java).

public interface WithTo<T> {
T withTo(String T);
}

public interface WithSubject<T> {
T withSubject(String T);
}

public interface WithBody<T> {
T withBody(String T);
}

public interface WithBuild<T> {
T build();
}

The good thing about those interfaces is, that they can be reused with all records sharing some common fields. This is especially useful for very generic properties such as id or, createdBy which typically occur in many different records.

Next, implement the builder.

public record Email(String to, String subject, String body) {
public static WithTo<
WithSubject<
WithBody<
WithBuild<Email>>>>
builder() {
return (to) ->
(subject) ->
(body) ->
() -> new Email(to, subject, body);
}
}

This might be slightly harder to read, but is far less work. For developers familiar with other languages having currying as a more central language feature than Java, this even might not be the case.

Now call the builder.

Email.builder()
.withTo("john.smith@example.com")
.withSubject("New article on builders")
.withBody("...")
.build();

The syntax is slightly different, since there is no need for an additional class.

Apart from the faster implementation of builders, there are two major advantages of this fluent builder over the classic builder from my perspective.

Fixed Initialization Order

The order of initializing fields is fixed by defining the chain of interfaces. This means, whenever an object is created, it is initialized in the same order.

While one could write

        .withBody("...") // body first!
.withTo("john.smith@example.com")
.withSubject("New article on builders")
.build();

this would not be possible with a fluent builder. Some people might see this as a disadvantage. My personal experience is that it is helpful to have a fixed order. It is really hard to compare different code snippets if there are a dozen properties initialized in a different order.

Furthermore, IDEs will always suggest which property to initialize next. When creating an object, one does not even need to think about what has been already initialized and what a good order would be.

With a classic builder, the same property may be initialized several times.

        .withBody("...") // first time
.withTo("john.smith@example.com")
.withSubject("New article on builders")
.withBody("...") // second time
.build();

This may not be an issue in many cases (since the same value is simply set twice), it is certainly not desired. Fixed initialization order prevents this kind of errors.

Forced Initialization

As with a standard constructor, all properties need to be set, even if they are not mandatory and could be kept uninitialized. This is different from a classic builder. As mentioned earlier, leaving out e.g. to would not fail at compile time with a classic builder

new Email.Builder()
.withSubject("New article on builders")
.withBody("...")
.build();

The fluent builder would prevent compilation. It is still possible though to explicitly null some properties. So sending an email with no subject can still be done

new Email.Builder()
.withTo("john.smith@example.com")
.withSubject(null)
.withBody("...")
.build();

but it is less likely to simply forget about the subject.

Conclusion

This advanced builder pattern came into my mind after implementing too many builders and reading about currying, traits, and finally, the article Introduction to the Fluent Builder Pattern.

I have to admit that I just played around with this pattern, so it has yet to be verified in some real-world projects.

A Final Word on Records

I chose to use a Java record as an example for this article to keep code snippets short. In general, it might be a good idea to force usage of the builder by making the constructor called by the builder private. This is not possible with records at the moment. So one may still decide to use classes instead of records, even if using a JDK supporting records.

--

--

Responses (1)