Builder Design pattern in Java | Creational Design Pattern

Published on August 19, 2024

Understanding the Builder Design Pattern with Java

When working with classes that have many parameters, especially optional ones, traditional constructors can quickly become cumbersome and error-prone. This is where the Builder Design Pattern shines. The Builder pattern provides a flexible solution for constructing complex objects, making your code more readable and maintainable. In this post, we’ll explore how to implement the Builder pattern in Java by walking through an example.

A Builder Pattern solves the issue with a large number of optional parameters and inconsistent states by providing a way to build the object step-by-step and provide a method that will actually return the final Object.

Consider a scenario where you need to create an Employee object. The Employee class has several fields, such as name, company, hasCar, and hasBike. Not all of these fields may be necessary for every instance of Employee, leading to the creation of multiple constructors. This approach, known as the telescoping constructor pattern, can quickly become unwieldy, as shown below:

public class Employee {
    public Employee(String name, String company) { ... }
    public Employee(String name, String company, boolean hasCar) { ... }
    public Employee(String name, String company, boolean hasCar, boolean hasBike) { ... }
    // More constructors as new fields are added...
}

As the number of fields increases, the number of constructor combinations explodes, making the code difficult to understand and maintain.

The Builder Pattern: A Clean Solution

The Builder pattern offers a more elegant solution. It allows you to construct an object step-by-step by setting only the fields you need, and it ensures that the final object is always in a consistent state.

Here’s how you can implement the Builder pattern in Java:

class Employee {
 
    private String name;
    private String company;
    private boolean hasCar;  // optional
    private boolean hasBike; // optional
 
    // Private constructor accessed by the Builder
    private Employee(EmployeeBuilder employeeBuilder) {
        this.name = employeeBuilder.name;
        this.company = employeeBuilder.company;
        this.hasCar = employeeBuilder.hasCar;
        this.hasBike = employeeBuilder.hasBike;
    }
 
    // Getters for accessing fields
    public String getName() { return name; }
    public String getCompany() { return company; }
    public boolean hasCar() { return hasCar; }
    public boolean hasBike() { return hasBike; }
 
    // Static nested Builder class
    public static class EmployeeBuilder {
        private final String name;
        private final String company;
        private boolean hasCar;
        private boolean hasBike;
 
        // Constructor for required fields
        public EmployeeBuilder(String name, String company) {
            this.name = name;
            this.company = company;
        }
 
        // Setter methods for optional fields
        public EmployeeBuilder setHasCar(boolean hasCar) {
            this.hasCar = hasCar;
            return this;
        }
 
        public EmployeeBuilder setHasBike(boolean hasBike) {
            this.hasBike = hasBike;
            return this;
        }
 
        // Method to construct the final Employee object
        public Employee build() {
            return new Employee(this);
        }
    }
 
    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", company='" + company + '\'' +
                ", hasCar=" + hasCar +
                ", hasBike=" + hasBike +
                '}';
    }
}

How the Builder Pattern Works

  1. Encapsulation of Construction Logic: The EmployeeBuilder class is responsible for building Employee objects. It holds the same fields as Employee and provides setter methods for each optional field. The build() method in the EmployeeBuilder class calls a private constructor of Employee, passing itself as an argument. This constructor copies the values from the builder to the actual Employee object.

  2. Immutable and Consistent Objects: The Employee class is designed to be immutable, meaning its state cannot change after it is created. The Builder pattern ensures that the object is always created in a consistent state because the only way to instantiate Employee is through the EmployeeBuilder.

  3. Flexibility and Readability: The Builder pattern provides flexibility in object construction. For example, you can create an Employee with just the required fields, or you can set any combination of optional fields:

    Employee employee1 = new Employee.EmployeeBuilder("John", "Wipro").build();
    Employee employee2 = new Employee.EmployeeBuilder("Roy", "TCS").setHasBike(true).build();
    Employee employee3 = new Employee.EmployeeBuilder("John", "Google")
                                     .setHasCar(true)
                                     .setHasBike(true)
                                     .build();

    This approach is much more readable and maintainable than a series of overloaded constructors.

Example Output

When you run the code above, it produces the following output:

Employee{name='John', company='Wipro', hasCar=false, hasBike=false}
Employee{name='Roy', company='TCS', hasCar=false, hasBike=true}
Employee{name='John', company='Google', hasCar=true, hasBike=true}

Conclusion

The Builder pattern is a powerful tool when working with complex objects that require many parameters. It simplifies the construction process, reduces errors, and produces clean, maintainable code. By separating the construction logic from the actual object, it also promotes the principle of separation of concerns, making your code more modular and easier to understand. Whether you're dealing with mandatory and optional fields or ensuring immutability, the Builder pattern is a go-to solution in your Java toolkit.