Java Builder Pattern: Build Complex Objects Efficiently

The builder pattern, as the name implies, is an alternative way to construct complex objects. This pattern should be used when we want to build different immutable objects using the same object-building process.

1. The GoF Builder Pattern

Before starting the discussion, I want to make it clear that the builder pattern that we are going to discuss in this post, is slightly different from what is mentioned in GangOfFour’s “Design Patterns” book. The book says:

The builder pattern is a design pattern that allows for the step-by-step creation of complex objects using the correct sequence of actions. The construction is controlled by a director object that only needs to know the type of object it is to create.

The book gives examples like below:

I find it hard to use the above example in real-life programming and applications. The above process is very similar (not exactly) to the abstract factory pattern, where we find a factory (or builder) for a specific type of object, and then the factory gives us a concrete instance of that object.

The only big difference between the builder pattern and the abstract factory pattern is that the builder provides us more control over the object creation process, and that’s it. Apart from that, there are no major differences.

In one sentence, abstract factory pattern is the answer to "WHAT" and the builder pattern to "HOW".

Now from here, we will start discussing the builder pattern in the way I find it useful especially in practical cases.

2. Definition of Builder Pattern

Let’s start by giving a definition of the builder pattern:

Builder pattern aims to “Separate the construction of a complex object from its representation so that the same construction process can create multiple different representations.”

A builder pattern should be more like a fluent interface. A fluent interface is normally implemented by using method cascading (or method chaining) as we see in lambda expressions.

3. Where do we require the Builder Pattern?

We already know the benefits of immutability and immutable instances in an application. If you have any questions about it, let me remind you of the String class in Java. As I already said, the builder pattern helps us in creating immutable classes with a large set of state attributes.

Let’s discuss a common problem in our application. The primary entity in any user management module is User, let’s say. Ideally and practically as well, once a User object is fully created, we will not want to change its state. It simply does not make sense, right?

Now, let’s assume our User object has the following 5 attributes i.e. firstName, lastName, age, phone and address.

In normal practice, if we want to make an immutable User class, then we must pass all five pieces of information as parameters to the constructor. It will look like this:

public User (String firstName, String lastName, int age, String phone, String address){

	this.firstName = firstName;
	this.lastName = lastName;
	this.age = age;
	this.phone = phone;
	this.address = address;
}

Very good. Now what if only firstName and lastName are mandatory, and the rest 3 fields are optional. Problem !! We need more constructors. This problem is called the telescoping constructors problem.

public User (String firstName, String lastName, int age, String phone){ ...	}
public User (String firstName, String lastName, String phone, String address){ ...	}
public User (String firstName, String lastName, int age){ ...	}
public User (String firstName, String lastName){ ...	}

We will need some more like the above. Still can manage? Now let’s introduce our sixth attribute i.e. salary. Now it is a problem.

One way is to create more constructors, and another is to lose the immutability and introduce setter methods. You choose any of both options, and you lose something, right?

Here, the builder pattern will help you consume additional attributes while retaining the immutability of the User class.

4. Implementing Builder Pattern

Lombok’s @Builder annotation is a useful technique to implement the builder pattern.

Let’s solve the above problem in code. The given solution uses an additional class UserBuilder which helps us build desired User instance with all mandatory attributes and a combination of optional attributes without losing the immutability.

public class User
{
	//All final attributes
	private final String firstName; // required
	private final String lastName; // required
	private final int age; // optional
	private final String phone; // optional
	private final String address; // optional

	private User(UserBuilder builder) {
		this.firstName = builder.firstName;
		this.lastName = builder.lastName;
		this.age = builder.age;
		this.phone = builder.phone;
		this.address = builder.address;
	}

	//All getter, and NO setter to provde immutability
	public String getFirstName() {
		return firstName;
	}
	public String getLastName() {
		return lastName;
	}
	public int getAge() {
		return age;
	}
	public String getPhone() {
		return phone;
	}
	public String getAddress() {
		return address;
	}

	@Override
	public String toString() {
		return "User: "+this.firstName+", "+this.lastName+", "+this.age+", "+this.phone+", "+this.address;
	}

	public static class UserBuilder
	{
		private final String firstName;
		private final String lastName;
		private int age;
		private String phone;
		private String address;

		public UserBuilder(String firstName, String lastName) {
			this.firstName = firstName;
			this.lastName = lastName;
		}
		public UserBuilder age(int age) {
			this.age = age;
			return this;
		}
		public UserBuilder phone(String phone) {
			this.phone = phone;
			return this;
		}
		public UserBuilder address(String address) {
			this.address = address;
			return this;
		}
		//Return the finally consrcuted User object
		public User build() {
			User user =  new User(this);
			validateUserObject(user);
			return user;
		}
		private void validateUserObject(User user) {
			//Do some basic validations to check
			//if user object does not break any assumption of system
		}
	}
}

Below is the way we will use the UserBuilder in our code:

public static void main(String[] args) 
{
	User user1 = new User.UserBuilder("Lokesh", "Gupta")
	.age(30)
	.phone("1234567")
	.address("Fake address 1234")
	.build();

	System.out.println(user1);

	User user2 = new User.UserBuilder("Jack", "Reacher")
	.age(40)
	.phone("5655")
	//no address
	.build();

	System.out.println(user2);

	User user3 = new User.UserBuilder("Super", "Man")
	//No age
	//No phone
	//no address
	.build();

	System.out.println(user3);
}

Please note that the above-created User object does not have any setter method, so its state can not be changed once it has been built. This provides the desired immutability.

Sometimes developers may forget to add a few attributes to the User class. While adding a new attribute and containing the source code changes to a single class (SRP), we should enclose the builder inside the class (as in the above example). It makes the change more obvious to the developer that there is a relevant builder that needs to be updated too.

Sometimes I think there should be a destroyer pattern (opposite to builder) that should tear down certain attributes from a complex object in a systematic manner. What do you think?

5. Existing Implementations in JDK

All implementations of java.lang.Appendable are infact good examples of the use of the Builder pattern in Java. e.g.

Look how similar these implementations look to what we discussed above.

StringBuilder builder = new StringBuilder("Temp");

String data = builder.append(1)
		.append(true)
		.append("friend")
		.toString();

6. Advantages

Undoubtedly, the number of lines of code increases at least by double in the builder pattern, but the effort pays off in terms of design flexibility and much more readable code.

The parameters to the constructor are reduced and are provided in highly readable chained method calls. This way there is no need to pass in null for optional parameters to the constructor while creating the instance of a class.

Another advantage is that an instance is always instantiated in a complete state rather than sitting in an incomplete state until the developer calls (if ever calls) the appropriate “setter” method to set additional fields.

Finally, we can build immutable objects without much complex logic in the object-building process.

7. Disadvantages

Though the Builder pattern reduces some lines of code by eliminating the need for setter methods, it still doubles up total lines by introducing the builder object. Furthermore, although client code is more readable, the client code is also more verbose. However, for me, readability weighs more than lines of code.

That’s the only disadvantage I can think of.

Happy Learning !!

Comments

Subscribe
Notify of
guest
44 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode