DEV Community

Cover image for Builder Design Pattern
Srishti Prasad
Srishti Prasad

Posted on

Builder Design Pattern

The Builder Pattern is a creational design pattern that provides a way to construct complex objects step by step. It separates the construction of an object from its representation, allowing the same construction process to create different types and representations of objects.

Now the question arises why do we need builder design pattern, what problem do we face ?

While creating object when object contain many attributes there are many problem exists:

  1. We have to pass many arguments to create object
  2. some parameters might be optional
  3. factory class takes all responsibility for creating object. If the object is heavy then all complexity is the part of factory class

So in builder pattern provides several advantages, especially when dealing with the construction of complex objects and is used to create object step by step and finally return object with desired values of attribute.

The Builder Pattern can be explained with a simple analogy: ordering a customized pizza.

Scenario without the Builder Pattern:

Imagine you go to a pizzeria 🍕 and are given only one option to order a pizza: you have to specify everything in one go — the type of crust, the size, the sauce, the toppings, and the extras. If you miss or incorrectly specify something, the pizza 🍕 might not turn out the way you want, and you’d have to redo the entire process from scratch. This is like using a constructor with many parameters — it’s error-prone and difficult to manage, especially when there are many options or optional items.

Example without Builder:

"I want a pizza 🍕 with thin crust, medium size, tomato sauce, mushrooms, cheese 🧀, no olives 🍅, and extra basil 🍀." If you miss something, you’d have to start over!
Scenario with the Builder Pattern:
Now, think of a more flexible approach where the pizza maker asks you step by step for your preferences:

First, they ask what size you want.
Then, they ask about the crust type.
Next, they ask about the toppings.
Finally, they ask if you'd like any extras, like extra cheese or a specific sauce.
At each step, you make decisions based on what you want, and at the end, you get a pizza exactly tailored to your taste. You didn’t have to worry about remembering or specifying everything at once. Each choice built on the previous one.

Example with Builder:

"I want a medium pizza."
"I’d like thin crust."
"Add mushrooms and cheese."
"No olives, please."
"Add extra basil."
The Builder Pattern allows for a step-by-step, structured process, where you build your object (or pizza!) incrementally, leading to more flexibility and fewer mistakes.

Here are few points I have observed how builder pattern gives you an edge over other approaches like telescoping constructors or setters:

1). Simplifies Object Construction with Many Parameters

  • When an object has many optional or mandatory parameters, using a standard constructor can become confusing and error-prone.
  • The Builder Pattern provides a clear, step-by-step approach to object creation, making it easier to set only the necessary properties without worrying about parameter order.

Example: Instead of this confusing constructor:

const car = new Car('Tesla', 'Model S', 2024, 'Red', true, false);
Enter fullscreen mode Exit fullscreen mode

You get this readable code:

const car = new CarBuilder('Tesla', 'Model S')
              .setYear(2024)
              .setColor('Red')
              .addGPS()
              .build();
Enter fullscreen mode Exit fullscreen mode

2). Supports Fluent Interface (Method Chaining)

  • The fluent interface allows methods to return the builder object itself, enabling method chaining. This makes the code more intuitive and readable, as it provides a logical, step-by-step process for setting attributes.

Advantage: Each method call reads like a sequence of instructions for constructing the object, improving readability.

3). Handles Optional Parameters Gracefully

  • With constructors, it’s difficult to deal with optional parameters without resorting to passing null values or creating many overloaded constructors.
  • The Builder Pattern allows you to include only the parameters you need, without worrying about providing defaults or placeholders for every parameter.

Without Builder (using constructor):

const car = new Car('Tesla', 'Model S', 2024, null, true, false); // Confusing

Enter fullscreen mode Exit fullscreen mode

With Builder:

const car = new CarBuilder('Tesla', 'Model S')
              .setYear(2024)
              .addGPS()
              .build();  // Clean and understandable

Enter fullscreen mode Exit fullscreen mode

4). Promotes Immutability

  • Once the object is built, it’s often immutable—you can’t change its state after creation. This ensures that the constructed object is in a valid and stable state, making your code safer and less prone to bugs.

Advantage: The object is fully constructed and valid after the build() method is called, reducing chances for errors due to partially constructed objects.

5). Easier to Extend

  • The Builder Pattern is easily extendable. When new attributes or features are added, the builder can be modified without affecting existing code. Without Builder: If you add a new parameter to the constructor, you must change every instantiation of the object across the codebase.

With Builder: You simply add a new setter method to the builder, without affecting the places where the object is built.

Example: Adding an engine type:

const car = new CarBuilder('Tesla', 'Model S')
              .setYear(2024)
              .setColor('Red')
              .setEngineType('Electric')
              .build();

Enter fullscreen mode Exit fullscreen mode

6). Improved Readability and Maintainability

  • The Builder Pattern offers high readability by breaking down the construction process into small, easy-to-understand steps.
  • This makes the code more self-documenting—you can clearly see how an object is being constructed, without having to look up what each argument in a constructor represents. Advantage: It’s easier to maintain and modify the code since each method clearly defines what part of the object it’s modifying.

7). Prevents Object Inconsistency

  • The Builder ensures that all required parameters are set before the object is built, preventing the creation of inconsistent or incomplete objects.
  • This validation process happens at the time of building, ensuring the object is in a valid state when it's created.

Without Builder: You might create objects that are incomplete or improperly initialized.

With Builder: The builder pattern guarantees a consistent and fully constructed object.

8). Reduces the Need for Overloaded Constructors

  • Without the Builder Pattern, you might end up with multiple overloaded constructors to accommodate different combinations of parameters. Advantage: With the builder, you don’t need multiple versions of constructors—just one clear, flexible building process.

Let’s compare object creation with and without the Builder Pattern using a real-world example where we are creating a Car object that has multiple properties, some of which are optional.

Without the Builder Pattern

1). Using Constructor with Many Parameters

class Car {
  constructor(make, model, year, color, hasGPS, hasSunroof) {
    this.make = make;
    this.model = model;
    this.year = year;
    this.color = color;
    this.hasGPS = hasGPS || false;     // Optional parameter
    this.hasSunroof = hasSunroof || false; // Optional parameter
  }

  describe() {
    return `${this.year} ${this.make} ${this.model} in ${this.color} color with GPS: ${this.hasGPS} and Sunroof: ${this.hasSunroof}`;
  }
}

// Creating the object
const car = new Car('Tesla', 'Model S', 2024, 'Red', true, false);
console.log(car.describe());

Enter fullscreen mode Exit fullscreen mode

With the Builder Pattern

2). Using the Builder Pattern

// Car class remains simple, no complex constructor
class Car {
  constructor(builder) {
    this.make = builder.make;
    this.model = builder.model;
    this.year = builder.year;
    this.color = builder.color;
    this.hasGPS = builder.hasGPS;
    this.hasSunroof = builder.hasSunroof;
  }

  describe() {
    return `${this.year} ${this.make} ${this.model} in ${this.color} color with GPS: ${this.hasGPS} and Sunroof: ${this.hasSunroof}`;
  }
}

// CarBuilder class for step-by-step object creation

class CarBuilder {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }

  setYear(year) {
    this.year = year;
    return this;
  }

  setColor(color) {
    this.color = color;
    return this;
  }

  addGPS() {
    this.hasGPS = true;
    return this;
  }

  addSunroof() {
    this.hasSunroof = true;
    return this;
  }

  build() {
    return new Car(this); // Builds and returns the Car object
  }
}

// Creating the object using the builder

const car = new CarBuilder('Tesla', 'Model S')
  .setYear(2024)
  .setColor('Red')
  .addGPS()
  .build();

console.log(car.describe());

Enter fullscreen mode Exit fullscreen mode

I would love to hear how you've applied these ideas to your work? Share your thoughts or questions in the comments below—I’d love to hear from you.

Thank you for joining me on this learning journey!

Top comments (2)

Collapse
 
wiscaksono profile image
Wisnu Wicaksono

this guy wrote an article about javascript, with a gopher thumbnail :)

Collapse
 
shifi profile image
Shifa Ur Rehman

😂 talk about genetics