❯ Guillaume Laforge

Functional builders in Java with Jilt

A few months ago, I shared an article about what I called Java functional builders, inspired by an equivalent pattern found in Go. The main idea was to have builders that looked like this example:

LanguageModel languageModel = new LanguageModel(
    name("cool-model"),
    project("my-project"),
    temperature(0.5),
    description("This is a generative model")
);

Compared to the more tranditional builder approach:

  • You’re using the new keyword again to construct instances.
  • There’s no more build() method, which felt a bit verbose.

Compared to using constructors with tons of parameters:

  • You have methods like in traditional builders, that say what each parameter is about (name(), temperature()…) a bit similar to named parameters in some programming languages.

The approach I followed was to take advantage of lambda functions under the hood:

public static ModelOption temperature(Float temperature) {
    return model -> model.temperature = temperature;
}

However, there were a few downsides:

  • Of course, it’s not very conventional! So it can be a bit disturbing for people used to classical builders.
  • I didn’t make the distinction between required and optional parameters (they were all optional!)
  • The internal fields were not final, and I felt they should be.

Discovering Jilt

When searching on this topic, I found Adam Ruka’s great annotation processor library: Jilt.

One of the really cool features of Jilt is its staged builder concept, which makes builders very type-safe, and forces you to call all the required property methods by chaining them. I found this approach very elegant.

Adam heard about my functional builder approach, and decided to implement this new style of builder in Jilt. There are a few differences with my implementation, but it palliates some of the downsides I mentioned.

Let’s have a look at what functional builders looks like from a usage standpoint:

LanguageModel languageModel = languageModel(
    name("cool-model"),
    project("my-project"),
    temperature(0.5),
    description("This is a generative model")
);

Compared to my approach, you’re not using constructors (as annotation processors can’t change existing classes), so you have to use a static method instead. But otherwise, inside that method call, you have the named-parameter-like methods you’re used to use in builders.

Here, name(), project() and temperature() are mandatory, and you’d get a compilation error if you forgot one of them. But description() is optional and can be ommitted.

Let’s now look at the implementation:

import org.jilt.Builder;
import org.jilt.BuilderStyle;
import org.jilt.Opt;

import static jilt.testing.LanguageModelBuilder.*;
import static jilt.testing.LanguageModelBuilder.Optional.description;
//...
LanguageModel languageModel = languageModel(
    name("cool-model"),
    project("my-project"),
    temperature(0.5),
    description("This is a generative model")
);
//...
@Builder(style = BuilderStyle.FUNCTIONAL)
public record LanguageModel(
    String name,
    String project,
    Double temperature,
    @Opt String description
) {}

I used a Java record but it could be a good old POJO. You must annotate that class with the @Builder annotation. The style parameter specifies that you want to use a functional builder. Notice the use of the @Opt annotation to say that a parameter is not required.

Derived instance creation

Let me close this article with another neat trick offered by Jilt, which is how to build other instances from existing ones:

@Builder(style = BuilderStyle.FUNCTIONAL, toBuilder = "derive")
public record LanguageModel(...) {}
//...
LanguageModel derivedModel = derive(languageModel, name("new-name"));

By adding the toBuilder = "derive" parameter to the annotation, you get the ability to create new instances similar to the original one, but you can change both required and optional parameters, to derive a new instance.

Time to try Jilt!

You can try functional builders in Jilt 1.6 which was just released a few days ago!