Builder Design Pattern In Java

Narendra Koli
Javarevisited
Published in
8 min readApr 28, 2024

The Builder Design Pattern: Simplifying Complex Object Construction

Have you ever needed to build complex objects but wanted to avoid a constructor with too many parameters?

This blog will explore the Builder Pattern through a lively, conversational exchange between a curious coder and a guiding mentor.

Conversation Starts:

Curious Coder: Hi there, Guiding mentor. Could you please explain the Builder design pattern to me?

Guiding Mentor: Sure.

Imagine we released our code with a User class and three mandatory fields: userId, firstName, and lastName. Our library is being used by thousands of clients for their applications.

This is what our class looks like:

public class Main {

public static void main(String[] args) {
User user1 = new User("1", "ABC", "DEF");
System.out.println(user1);
}
}

class User {

private final String userId;
private final String firstName;
private final String lastName;

public User(String userId, String firstName, String lastName) {
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
}

public String getUserId() {
return userId;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

@Override
public String toString() {
return "User{" +
"userId='" + userId + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
'}';
}
}

Output:

Now, we want to make another release and add an optional field called age.

Now, it’s your turn to implement this. 😁

Curious Coder: Sure. I will implement this and get back to you.

A few moments later:

I created a new constructor because we can’t modify the existing one. That constructor might be used by other clients, so modifying it would break the code for those clients. Also, the age field is optional, so I added a new constructor.

public class Main {

public static void main(String[] args) {
User user1 = new User("1", "ABC", "DEF");
System.out.println(user1);
User user2 = new User("2", "GHI", "JKL", 30);
System.out.println(user2);
}
}

class User {

private final String userId;
private final String firstName;
private final String lastName;
private int age;

public User(String userId, String firstName, String lastName) {
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
}

// new constructor with age field
public User(String userId, String firstName, String lastName, int age) {
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

public String getUserId() {
return userId;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

public int getAge() {
return age;
}

@Override
public String toString() {
return "User{" +
"userId='" + userId + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", age=" + age +
'}';
}

}

Output:

Guiding Mentor: Makes sense.

Now, Imagine we released this code and want to make another release with a new optional field called weight.

Curious Coder: Sure, I will get back to you.

A few moments later:

I added two new constructors, replicating the existing two constructors and adding new field weight to ensure we allowed all mandatory and optional field combinations.

public class Main {

public static void main(String[] args) {
User user1 = new User("1", "ABC", "DEF");
System.out.println(user1);
User user2 = new User("2", "GHI", "JKL", 30);
System.out.println(user2);
User user3 = new User("3", "MNO", "PQR", 30, 75.50);
System.out.println(user3);
User user4 = new User("4", "STU", "VWX", 75.50);
System.out.println(user4);
}
}

class User {

private final String userId;
private final String firstName;
private final String lastName;
private int age;
private double weight;

public User(String userId, String firstName, String lastName) {
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
}

public User(String userId, String firstName, String lastName, int age) {
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

// Two new constructors for the weight field.
public User(String userId, String firstName, String lastName, int age, double weight) {
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.weight = weight;
}

public User(String userId, String firstName, String lastName, double weight) {
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
this.weight = weight;
}


public String getUserId() {
return userId;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

public int getAge() {
return age;
}

public double getWeight() {
return weight;
}

@Override
public String toString() {
return "User{" +
"userId='" + userId + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", age=" + age +
", weight=" + weight +
'}';
}
}

Output:

Guiding Mentor: Did you notice a problem?

Curious Coder: To add even a single field, we need to replicate every constructor, which will be difficult to manage in the future.

Guiding Mentor: Absolutely. But there are more problems with it. Let’s look at them one by one.

  • Too many constructors: We need to add too many constructors to introduce a single optional field.
  • Code Readability Issue: The constructor invocation code makes it difficult to determine which field we are trying to set. If we have too many constructors, we must jump into the User class to see which is being called and which fields are being set.
  • Immutability Issue: Building immutable objects with constructors, especially when dealing with numerous optional parameters, is challenging because you need to pass all the values through the constructor.

What do you think about how can we solve this problem?

Curious Coder: Can we avoid adding a constructor to the class and have setter methods only? This way, clients can create instances and call the setters to set each field they want.

Guiding Mentor:

Using setters to set all fields of an object instead of using a Builder pattern can introduce several problems:

  • Lack of immutability: One of the primary concerns with using setters is that the object can usually be changed after it’s constructed.
  • Incomplete state: With setters, an object can be in an incomplete state after it’s constructed, especially if not all fields are mandatory.
  • The construction process is not clean.

Curious Coder: Yes, You are correct.

Guiding Mentor: This is where the Builder design pattern comes to the rescue.

Let’s learn it from a real-world example of a Subway sandwich.

Photo by melika zibaee on Unsplash

When you walk into a Subway restaurant to order a sandwich, you are faced with a series of choices: what type of bread, what kind of cheese, what sauces, and what vegetables. This process is not just making a sandwich; it’s about customizing it to your preferences, step by step.

This is analogous to the Builder design pattern, where a complex object is constructed through a series of steps, allowing for a highly customizable end product without complexity.

Using the Builder pattern, you can encapsulate the object’s construction and allow the process to be done in multiple steps.

Let’s look at how it can be done in our previous example.

  • In Java, the Builder pattern is often implemented using an inner class, which provides several advantages. The inner Builder class is typically static and nested within its building class.
  • This setup allows the Builder to access all the fields and constructors of its outer class, even if they are private.
public class Main {

public static void main(String[] args) {

User user1 = new User.UserBuilder("1", "ABC", "DEF").build();
System.out.println(user1);

User user2 = new User.UserBuilder("2", "GHI", "JKL").age(30).build();
System.out.println(user2);

User user3 = new User.UserBuilder("3", "MNO", "PQR").age(30).weight(75.50).build();
System.out.println(user3);

User user4 = new User.UserBuilder("4", "STU", "VWX").weight(75.50).build();
System.out.println(user4);

}
}

class User {

private final String userId;
private final String firstName;
private final String lastName;
private final int age;
private final double weight;

private User(UserBuilder userBuilder) {
this.userId = userBuilder.userId;
this.firstName = userBuilder.firstName;
this.lastName = userBuilder.lastName;
this.age = userBuilder.age;
this.weight = userBuilder.weight;
}

public String getUserId() {
return userId;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

public int getAge() {
return age;
}

public double getWeight() {
return weight;
}

@Override
public String toString() {
return "User{" +
"userId='" + userId + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", age=" + age +
", weight=" + weight +
'}';
}

static class UserBuilder {

private final String userId;
private final String firstName;
private final String lastName;
private int age;
private double weight;

public UserBuilder(String userId, String firstName, String lastName) {
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
}

UserBuilder age(int age) {
this.age = age;
return this;
}

UserBuilder weight(double weight) {
this.weight = weight;
return this;
}

User build() {
return new User(this);
}
}
}

Output:

Now, compare your previous object creation step with this one.

Curious Coder: Yeah, this looks pretty good

If I need to introduce a new optional field, we can add the same field to the User and UserBuilder classes. That solves a problem.

Can you tell me the benefits and drawbacks of this Builder design pattern?

Guiding Mentor: We have already discussed the benefits of the Builder pattern, but let’s recap.

Benefits:

  • Improve readability and maintainability of code.
  • Flexibility in Object construction
  • Immutability of the Object
  • It separates the construction of the complex object from its representation, allowing the same construction process to create different representations.

Drawbacks:

  • Complexity: Implementing the Builder pattern can add additional complexity to the code.
  • Code Duplication: There can be some redundancy since you need to duplicate all fields from the original class to the builder class.
  • While increased readability is a significant advantage, it also makes the code more verbose. This can be considered overkill for simple objects and may unnecessarily bloat the codebase.

Curious Coder: Great. Now, I understand the Builder design pattern.

Again, thanks a lot for helping me improve my knowledge of design patterns.

Guiding Mentor: You are welcome.

Conversation Ends. 😊

I hope you enjoyed this article.

Thank you for dedicating a precious time to reading this blog post! 💖 🕒

If you enjoyed reading this blog post, you might also like my previous blog posts. 😊

Singleton design pattern: https://medium.com/javarevisited/singleton-design-pattern-in-java-feb854dd7903n

Adapter design pattern: https://medium.com/javarevisited/adapter-design-pattern-in-java-945164d18b7f

Strategy design pattern: https://medium.com/javarevisited/strategy-design-pattern-in-java-aa34732973db

--

--

Narendra Koli
Javarevisited

I am a Software Engineer who loves to write....❤️