Create Complex Java Objects Using Fluent Builder Pattern
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:
- Method Chaining — One method succeeds another method in a chain.
- 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 thePerson
object and for some reason, we create the object like this:
Person person = Person.builder().age(25).build();
As you can see, we created thePerson
object 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 thePerson
object. Now we can move to the next step i.e., integrating fluent interface to attain method chaining in thePerson
object 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
. student
object will have an extra attribute school
while employee
object 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 thestudent
method in the builder chain allows to invoke setSchool
method. Whereas, employee
method 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 thePersonBuilder
object.
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.