Create Complex Java Objects Using Fluent Builder Pattern

Akhil Gupta
6 min readMay 3, 2023

--

Intended Audience

I have seen some articles which touch on the concepts related to Fluent Builder Pattern at a surface level. However, I can assure you that it is a very powerful object-creation pattern and there is much more to explore. Dive into the article if you want to learn a couple of tricks!

Introduction

I am sure if you have worked on Java spring-boot applications you would have encountered scenarios in which you are required to create complex Java objects which are either used in the business logic or as request/response objects. Typically, to reduce the lot of boilerplate code required while defining the object such as setters, getters, constructors, etc. are handled using the builder pattern. The builder pattern can be utilized by adding @Builder annotation (provided by Lombok library) on the object class and it has become immensely popular because of its ability to enhance code readability.

The Fluent Builder pattern is an enhancement of the Builder pattern and combines the concepts of the Builder pattern with the Fluent Interface pattern. The Fluent Builder pattern aims to create readable and concise code by allowing method chaining while building complex objects. Some advantages of using fluent builder in conjunction with builder pattern are:

  1. Method Chaining — One method succeeds another method in a chain.
  2. Conditional Chaining — Branching the method chain based on the type of object to be created.

Method Chaining

Let’s take one example to dive into method chaining. Suppose we have to create the Person object with attributes name and age.

@Builder
public class Person {
private String name;
private int age;
}
public class Main {
public static void main (String args[]) {
Person person = Person.builder().name("John").age(25).build();
}
}

So far so simple, using @Builder annotation this can be achieved quite easily. However, there can be instances in which some information is mandatory and objects on creation should have that information. In this instance let’s assume that the name must always be present in thePersonobject and for some reason, we create the object like this:

Person person = Person.builder().age(25).build();

As you can see, we created thePersonobject without providing a name, which might break some functionalities that depend on the same. These situations can especially arise when the developer doesn’t have the awareness related to the object contract. To avoid such situations we can incorporate the fluent interface along with the builder pattern. To understand the fluent builder pattern let’s first understand how the builder pattern works.

In the standard Builder pattern, object creation is typically facilitated through a separate builder class or a static inner builder class within the class being built. The builder class contains methods for setting the values of different attributes of the object being constructed. Each method in the builder construct returns the same instance of the static inner class. Finally, abuild()method is required which creates the instance of the object that we wanted to create.

public class Person {
private String name;
private int age;
private Person(String name, int age) {
this.name = name;
this.age = age;
this.address = address;
}
public static PersonBuilder builder() {
return new PersonBuilder();
}
public static class PersonBuilder {
private String name;
private int age;
public PersonBuilder name(String name) {
this.name = name;
return this;
}
public PersonBuilder age(int age) {
this.age = age;
return this;
}
public Person build() {
return new Person(name, age, address);
}
}
}

As can be seen in the code, we have created PersonBuilder class that is being used to propagate through various states of the object creation, and eventually, thebuild()method is invoked to return the instance of thePersonobject. Now we can move to the next step i.e., integrating fluent interface to attain method chaining in thePersonobject creation. Here is the full code for the same:

@Data
@AllArgsConstructor
public class Person {
private String name;
private int age;
public interface Builder {
Person build();
}
public interface SetAge {
Builder setAge(int age);
}
public interface SetName {
SetAge setName(String name);
}
public static SetName builder() {
return new PersonBuilder();
}
public static class PersonBuilder implements SetAge, SetName, Builder {
private String name;
private int age;
private PersonBuilder() {
}
@Override
public Builder setAge(int age) {
this.age = age;
return this;
}
@Override
public Person build() {
return new Person(name, age);
}
@Override
public SetAge setName(String name) {
this.name = name;
return this;
}
}
}

We have created the interfaces SetAge and SetName which defines the contract for the builder. The SetAge and SetName interfaces define methods for setting the age and name attributes, respectively. They return the appropriate next-step interfaces to continue the method chaining and the Builder interface provides the build() method to construct the Person object which is basically the final step of object creation. The PersonBuilder class implements all the builder interfaces. It also maintains the temporary attribute values (name and age) and provides the methods for setting those values. Each setter method returns the next step interface, allowing for method chaining. Using this, we can now create the Person object in the following manner:

Person person = Person.builder().setName("John Doe").setAge(25.build();

In the example above, we start by calling the static builder() method of the Person class, which returns an instance of SetName interface. We can then chain the setter methods setName("John Doe") and setAge(25) to set the desired attribute values. Finally, we call the build() method to construct the Person object. The method chaining allows for a more concise and readable syntax, as each setter method returns the appropriate next-step interface, allowing us to immediately chain the next method call.

One of the biggest advantages of method chaining is that errors related to object creation can be identified during compile time (detected by IDE) instead of at the runtime which ensures that the code is less susceptible to bugs in the production environment. It especially becomes handy when a developer unfamiliar with the business logic and SLAs has to work on the object creation for feature requirements.

Note:- Here I have intentionally used method names different from default @Builder generated methods just in case you want to use both, you can approach it in this way.

Conditional Chaining

Now that we have understood how method chaining works in Fluent Design Pattern, we can dive into conditional chaining. For that let’s first add more attributes to Person object to make it more complex. Let’s say, we can create two types of Person, namely,student and employee. studentobject will have an extra attribute school while employeeobject will have the attribute company. Since we have two types of Person object, we will basically have two separate chains for each type. Here is a demonstration of the same.

Person person = Person.builder().student().setSchool("California")
.setName("John Doe").setAge(25).build();
Person person = Person.builder().employee().setCompany("Google")
.setName("John Moe").setAge(27).build();

As you can see from the above demonstration, calling thestudentmethod in the builder chain allows to invoke setSchool method. Whereas, employeemethod allows us to invoke setCompany. The object will only be allowed to build once thesetAge method is invoked. Here is the code which obtains our desired result:

@Data
@AllArgsConstructor
public class Person {
private String name;
private int age;
private Type type;
private String school;
private String company;
public interface Student {
SetSchool student();
}
public interface Employee {
SetCompany employee();
}
public interface SetAge {
PersonBuilder setAge(int age);
}
public interface SetName {
SetAge setName(String name);
}
public interface SetSchool {
SetName setSchool(String school);
}
public interface SetCompany {
SetName setCompany(String company);
}
public static PersonBuilder builder() {
return new PersonBuilder();
}
private enum Type {
STUDENT, EMPLOYEE
}
public static class PersonBuilder implements Student, Employee, SetAge, SetName, SetSchool, SetCompany {
private String name;
private Type type;
private int age;
private String school;
private String company;
private PersonBuilder() {
}
@Override
public SetSchool student() {
this.type = Type.STUDENT;
return this;
}
@Override
public SetCompany employee() {
this.type = Type.EMPLOYEE;
return this;
}
@Override
public PersonBuilder setAge(int age) {
this.age = age;
return this;
}
@Override
public SetName setSchool(String school) {
this.school = school;
return this;
}
@Override
public SetName setCompany(String company) {
this.company = company;
return this;
}
public Person build() {
return new Person(name, age, type, school, company);
}
@Override
public SetAge setName(String name) {
this.name = name;
return this;
}
}
}

Here, Student and Employee interfaces represent the different types of persons that can be created. These interfaces return the SetSchool, and SetCompany instance type respectively which ensures that the chain is split after selecting the type of Person object to create. Note that I have created the inner class in public scope. However, this can be changed by adding an abstraction layer on top of this so as to expose only the required method related to thePersonBuilderobject.

I find using Fluent Builder Pattern to create complex objects very satisfying as the entire logic for object creation gets abstracted and there is literally no way a developer can go wrong while creating the object even if he tries to. Hope you liked the article! Let me know if I missed anything or any piece of code that was not comprehensible in the comments.

--

--