Builder Design Pattern

What is Builder Pattern?

Builder is a creational design pattern that helps us create complex objects by providing a step-by-step construction process. It separates the construction of the object from its representation, which means we can use the same construction process to create different versions of the object.

Problem Statement

To understand the Builder design pattern, let's use the example of making pizzas. The process of making a pizza consists of a series of steps: first, we make the dough, and then we add the base, toppings, and sauce. Finally, we bake the pizza.

Solution Approach 1

Creating a complex object like a pizza requires step-by-step initialization of many fields. So, one solution is to define the initialization code inside a constructor with multiple parameters. To implement this, we can define a Pizza class with fields such as dough, base, and toppings, and use the constructor of the Pizza class to initialize these fields.

//Pizza class with constructor
public class Pizza {
    private String dough;
    private String base;
    private String toppings;
    private String sauce;
    private String bake;
    private String cheese;
    public Pizza(String dough, String base, String toppings, 
                        String sauce, String bake, String cheese) 
    {
        this.dough = dough;
        this.base = base;
        this.toppings =  toppings;
        this.sauce = sauce;
        this.bake = bake;
        this.cheese = cheese;
    }
    
    //...
}

Using the Pizza class constructor can help us create a pizza, but what if customers have different preferences? For example, one customer might want extra toppings and mozzarella cheese, while another customer might want no toppings at all. In this case, using a constructor alone may not be sufficient to handle different customization requests.

So we need to change our Pizza class and add a set of overloaded constructors like this:

//set of overloaded constructors which are added in Pizza class

public Pizza(String dough, String base, String toppings,
                  String sauce, String bake, String cheese)
{
    //...
}
    
public Pizza(String dough, String base, String sauce, 
                                String bake, String cheese)
{
    //pizza without toppings...  
}
public Pizza(String dough, String base, String toppings, 
                                 String sauce, String bake)
{
    //pizza without cheese...
}

Drawbacks

  • Technically, the set of overloaded constructors will give an error because the compiler will not be able to distinguish between the second and third constructors due to their same signature. So, this is a crucial drawback: If the class fields have similar data types, we will have difficulty using constructor overloading for object creation.
  • As the number of combinations of parameters increases, the number of constructors will increase. So this approach is inefficient for complex and heavy classes.

Takeaway: This approach is often termed a telescopic constructor pattern, an anti-pattern. So in place of using this pattern, we should try a better approach.

Solution Approach 2

To address the limitations of the telescopic constructor pattern, we can use setter methods to initialize the fields in a class. This way, we can specify the values for each field individually, rather than using a single constructor with a large number of parameters.

//... 
public void setDough(String dough) {
    this.dough = dough;
}
public void setBase(String base) {
    this.base = base;
 }
public void setSauce(String sauce) {
    this.sauce = sauce;
}
//and so on....
//...

Drawbacks

  • To use this approach, consumers have to call the setter methods in the correct order with the appropriate parameters. For example, if a consumer calls setSauce() before setDough(), they won't end up with a pizza. This can be a problem if the consumer doesn't know the proper order of the steps, as it's important to follow the recipe correctly to make a pizza. In simple words, using setter methods requires the consumer to have a good understanding of the process of creating the object.
  • In addition, if a consumer requests a specific type of pizza, such as a large Mexican pizza, the consumer code would have to call all the setter methods with the new values. This can be time-consuming and tedious, especially if there are many fields to initialize.

Takeaway

It's important to consider the order in which the steps are performed, and we don't want the consumer code to be responsible for this. Additionally, there may be many types of pizzas that require each step to be performed differently, which can be difficult to manage in the consumer code.

Builder pattern is a solution to these problems when creating a complex product. It will separate the construction process from the representation of the object, which will help us to use the same process to create different versions of the object.

An efficient solution using the Builder Pattern

We know that all pizzas follow the same process, but the implementation of the steps may vary. For example, an Italian pizza has different toppings and cheese from a Mexican pizza, but the steps to make both pizzas are the same. To handle this, we can separate the recipe from the process of creating the pizza.

To do this, we can hire a HeadChef who knows the recipe, and specialized cooks who can make specific types of pizzas. For example, an ItalianCook knows how to make an Italian pizza, and a MexicanCook knows how to make a Mexican pizza.

  • The HeadChef's role is to provide the recipe to the Cook, who follows the recipe and returns the finished pizza to the HeadChef. This way, the HeadChef can manage the overall process of making the pizza, while the Cooks handle the specific steps for each type of pizza.
  • From another perspective, each Cook is expected to follow the company's policies when making a pizza, and they are only allowed to use a common set of steps. However, they must implement these steps according to the requirements of their specific pizza. For example, a MexicanCook can't add a new component to the Pizza class. To solve this issue, we can introduce a Cook interface that declares all the steps involved in making a pizza.

In terms of classes and interfaces, the HeadChef class defines the steps in the correct order. The Cook interface consists of methods to set the fields required for the Pizza product, such as dough and sauce. Subclasses of Cook, like MexicanCook and ItalianCook, implement the methods provided in the Cook interface. Finally, the completed Pizza product is returned by one of the concrete subclasses of the Cook.

Components and Structure of Builder Pattern

Components and UML Structure of Builder Design Pattern

Builder (Cook): This is an interface that declares steps for constructing a product that is common to all of its subclasses or concrete builders.

Concrete Builders (ItalianCook, MexicanCook): These classes implement the methods of the Builder interface in different ways to meet the demands of consumers. Different concrete builders provide different implementations, so we can get different versions of the complex product. Note: Concrete builders may produce products that don’t follow the common interface.

Director (HeadChef): This class defines the proper order in which all the construction steps should be invoked, so we can create and reuse specific configurations of products. It uses the Builder interface to create an instance of the complex product.

Product (Pizza): The final objects returned by the concrete builders. While in this example all products belong to the same class (Pizza), it's possible for products to belong to different class hierarchies or interfaces. In that case, the consumer code would call the method of the Concrete builders directly to get the final product.

Client (Consumer): This part of the code associates one of the concrete builder's objects with the Director. The Director uses this builder object to construct the product. Note: The client can do this just once using the constructor parameters of the director. There can be another approach: the client can pass the builder object to the construction method of the director.

Implementation Steps of Builder Pattern

  1. Declare common construction steps in the builder interface for building all available products.
  2. Create concrete builder classes for each product representation and implement their construction steps.
  3. Add a separate method inside each concrete builder to retrieve the output of the construction. We cannot declare this method inside the builder interface because different builders may construct products that do not have a common interface. Therefore, we do not know the return type for such a method. Note: If working with products from a single hierarchy, we can simply add this method to the base interface.
  4. Create a director class and encapsulate various ways to construct a product using the builder object.
  5. In the client code, create both the builder and director objects, and pass the builder object to the director to start the construction process.

Implementation code Java

Builder Design Pattern Example

First, we create the Product class, Pizza, which has various fields and setter methods for those fields (e.g., setDough(), setBase(), etc). It's important to note that these fields can be objects of other classes as well.

public class Pizza {
    private String dough;
    private String base;
    private String toppings;
    private String sauce;
    private String bake;
    private String cheese;
        
    public void setDough(String dough) {
        this.dough = dough;
    }
    public void setBase(String base) {
        this.base = base;
    }
    public void setToppings(String toppings) {
        this.toppings = toppings;
    }
    public void setSauce(String sauce) {
        this.sauce = sauce;
    }
    public void setBake(String bake) {
        this.bake = bake;
    }
    public void setCheese(String cheese) {
        this.cheese = cheese;
    }
    public void showPizza() {
        System.out.println(dough+", "+base+", "+toppings+",   "+sauce+", "+bake+", "+cheese);
    }
}

Next, we create the Builder interface Cook, which includes all the steps involved in construction.

public interface Cook {
    public void buildDough();
    public void buildBase();
    public void buildToppings();
    public void buildSauce();
    public void buildBake();
    public void buildCheese();
    public Pizza getPizza();    
}

As mentioned above, It's important to consider whether the Builder interface should include a method for returning the instance of the Pizza class, or if the concrete subclasses should handle this on their own. 

  • This depends on the final representations of the complex product. If all the possible final representations belong to the same Pizza class, it makes sense to include a method in the Builder interface to retrieve the final product.
  • However, if the final products are very different, it may not be appropriate to represent them with the same base class or interface. In that case, we would need to handle the retrieval of the final product individually in the concrete builder classes.

We can then create subclasses of the Cook interface, such as MexicanCook and ItalianCook, which provide implementations of the construction steps declared in Cook. These concrete builders will apply all the steps and return the final product.

ItalianCook Class

public class ItalianCook implements Cook {
    private Pizza pizza;
    public ItalianCook() {
        this.pizza = new Pizza(); 
    }
    @Override
    public void buildDough() {
        pizza.setDough("Italian Dough");
    }
    @Override
    public void buildBase() {
        pizza.setBase("Italian Base");
    }
    @Override
    public void buildToppings() {
        pizza.setToppings("Italian Toppings");
    }
    @Override
    public void buildSauce() {
        pizza.setSauce("Italian Sauce");
    }
    @Override
    public void buildBake() {
        pizza.setBake("Bake");
    }
    @Override
    public void buildCheese() {
        pizza.setCheese("Cheese");
    }
        
    @Override
    public Pizza getPizza() {
        Pizza finalPizza = this.pizza;
        this.pizza = new Pizza();
        return finalPizza;
    }
}

MexicanCook Class

public class MexicanCook implements Cook {
    private Pizza pizza;
    public MexicanCook() {
        this.pizza = new Pizza(); 
    }
    @Override
    public void buildDough() {
        pizza.setDough("Mexican Dough");
    }
    @Override
    public void buildBase() {
        pizza.setBase("Mexican Base");
    }
    @Override
    public void buildToppings() {
        pizza.setToppings("Mexican Toppings");
    }
    @Override
    public void buildSauce() {
        pizza.setSauce("Mexican Sauce");
    }
    @Override
    public void buildBake() {
        pizza.setBake("Bake");
    }
    @Override
    public void buildCheese() {
        pizza.setCheese("Cheese");
    }
        
    @Override
    public Pizza getPizza() {
        Pizza finalPizza = this.pizza;
        this.pizza = new Pizza();
        return finalPizza;
    }
}

In the getPizza() method, we set the pizza field to a new Pizza object so that the same Cook object (or a concrete subclass of Cook) can be used to make more pizzas in the future.

Then, we declare the Director class, HeadChef, which is responsible for defining the order in which the steps are executed. There can be multiple Directors, or a single Director may have multiple construction processes.

public class HeadChef {
    private Cook cook;
    public HeadChef(Cook cook) {
        this.cook = cook;
    }
    public void makePizza() {
        cook.buildDough();
        cook.buildBase();
        cook.buildToppings();
        cook.buildSauce();
        cook.buildBake();
        cook.buildCheese();
    }
}

Finally, we create the Consumer code, which creates a concrete builder object and passes it to the Director (HeadChef). The Director then uses this builder object to apply the construction process, and the complex product is retrieved either through the Director or directly from the builder.

It's also possible to retrieve the final product from the builder because the Client typically configures the Director with the appropriate concrete builder. This means the Client knows which concrete builder will produce the desired product.

public static void main (String[] args){
    Cook cook = new ItalianCook();
    HeadChef headchef = new HeadChef (cook);
       
    headchef.makePizza();
    Pizza pizza = cook.getPizza();
    pizza.showPizza();
    cook = new MexicanCook();
    headchef = new HeadChef (cook);
    headchef.makePizza();
    pizza = cook.getPizza();
    pizza.showPizza();
}

Key Takeaway

  • Builder pattern allows us to separate two different responsibilities: the order of construction steps (the recipe), and the ingredients used in each step. We can use the same construction process (provided by the HeadChef) to create different representations of our object (the Pizza).
  • The Cook interface hides the details of the construction process from the consumer. This means that the consumer needs to associate a specific Cook through the HeadChef (by ordering a specific pizza) to get their order ready.
  • Adding new types of Cooks is easy because we just need to create a new subclass of Cook and implement it. In contrast, the previous approach required changing the existing consumer code.

Note: We can create additional classes like HeadChef, such as PrimeChef, which may have a different construction process depending on the needs of the company. Then, we can use the PrimeChef class with existing Cooks to get new, desirable pizzas! Alternatively, we can also define a new construction process within the HeadChef class itself.

In our example, we only considered a single construction process, but there can be many processes with slight differences. For example, perhaps a super-fast variant of pizza has some additional steps compared to a regular pizza.

public class HeadChef {
    private Cook cook;
    public HeadChef(Cook cook) {
        this.cook = cook;
    }
    public void makePizza() {
        //...
    }
    
    public void makeSuperFastPizza() {
        //...
    }
}

When to apply Builder Pattern?

We can use the Builder pattern when:

  • Using a telescopic constructor is not efficient.
  • The same construction process is used to create different representations of the complex product.
  • The process of creating the product is independent of the parts that make up the object and how they are assembled.

In our example of making pizzas, the process of creation is defined by the HeadChef class, while the parts that make up the object are implemented in the concrete builders (ItalianCook and MexicanCook). 

Notice that the Builder class (Cook) hides the internal representation of the Pizza product i.e. it hides the classes that define the various parts of the pizza and how they are assembled to create the final product. This means the process defined by the HeadChef class is independent of the parts of the pizza product.

It is also important to be able to easily add new representations and remove existing representations of the complex product. The Builder pattern allows for this flexibility.

Consequences

  • It makes modifying representations easy and flexible. As we’ve already seen, the builder object interacts with the director via an abstract interface. Also, each builder implements that interface in a particular manner to provide a specific representation of the product. Because the product is constructed through an interface, therefore, to add new representations, we’ve to add a new kind of concrete builder that implements the interface in a specific manner.
  • It reduces the responsibility of the consumer (client) code. The consumer only needs to associate the correct builder class with the appropriate director class (remember that there may exist more than one director).
  • It allows us to use the same construction process to build different representations of the complex product.
  • Additionally, the builder pattern gives finer control over the construction process. As compared to the factory method or abstract factory method which creates the objects in a single function call (that is, in one go!), the Builder pattern follows the step-by-step approach as directed by the director. The Builder interface depicts that, the final product is created by creating its various parts in a step-by-step manner. 
  • It makes the application flexible at the cost of increases in the complexity and number of classes in the code.

Applications of Builder Pattern

The Builder pattern is often used in creating objects that have multiple configuration options or parameters. Instead of providing a long list of constructor parameters or multiple overloaded constructors, Builder pattern provides a more flexible and readable way to construct objects.

Here are some popular applications of the Builder pattern:

  1. The StringBuilder class in Java libraries uses the Builder pattern. It provides a fluent interface and methods such as append() and insert() to concatenate and modify strings in a more efficient manner compared to concatenation using the + operator.
  2. The OkHttpClient class in the OkHttp library (used for making HTTP requests) uses the Builder pattern to construct a customized OkHttpClient instance. It provides various methods to set properties such as timeouts, interceptors, SSL configuration, and more.
  3. In Android development, the AlertDialog.Builder class is commonly used to create alert dialog boxes. It follows the Builder pattern to construct dialog objects with specific configurations, such as setting the title, message, buttons, and other attributes of the dialog.
  4. In software testing, the Builder pattern is frequently used to create test data objects. For example, Testing frameworks, like JUnit and TestNG, often use the Builder pattern to create test configuration objects. These builders enable developers to define and customize test setups, assertions, and other testing configurations using a fluent and readable API.
  5. Builder patterns can be used in serialization and deserialization processes. Here builder will read the serialized data and reconstruct the object with all its necessary attributes. For example, Google Gson (a popular Java library for JSON processing) includes the GsonBuilder class. It helps us to configure Gson with various options and provides a way to customize the JSON serialization and deserialization process.
  6. The RTF converter application of ET++ uses the Builder pattern. For a more detailed explanation, one can refer to the GoF book.

Relations with Other Patterns

  • Abstract Factory creates families of related objects and returns the product immediately. On the other side, the Builder pattern uses some construction steps before returning the product.
  • The Builder pattern can be used in combination with the Prototype pattern to construct complex objects by copying an existing object's structure.
  • We can combine Builder pattern with the Chain of Responsibility pattern to define a chain of builders, where each builder handles a specific construction step. If a builder cannot handle the step, it passes the responsibility to the next builder in the chain.
  • Builder can be implemented as Singleton.
  • Sometimes, we start the simple object-creation process using the factory method and based on evolving requirements, move towards complex object creation using Abstract Factory, Prototype, or Builder.
  • The Builder pattern can be used with the Command pattern to encapsulate construction requests as commands. This will provide more flexibility and customization during the construction process.
  • Builder pattern can use the Strategy pattern to vary the construction algorithm dynamically. Different builder strategies can be used interchangeably to construct different variations of the same product.

Conclusion

We observed some possible, but inefficient approaches, such as using telescopic constructors and constructing objects using setter methods in the consumer code. Then, we learned about the Builder pattern and explored how it addresses the issues present in the previous approaches.

For example, it allows for more manageable consumer code and ensures the correct order of the construction process. Additionally, we examined its applicability and its impact on the overall creational process.

Thanks to Ankit Nishad for his contribution in creating the first version of this content. If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning, Enjoy oops!

Share Your Insights

☆ 16-week live DSA course
☆ 16-week live ML course
☆ 10-week live DSA course

More from EnjoyAlgorithms

Self-paced Courses and Blogs

Coding Interview

Machine Learning

System Design

Our Newsletter

Subscribe to get well designed content on data structure and algorithms, machine learning, system design, object orientd programming and math.