Builder Pattern

intermediate 2-4 YOE lld design-pattern creational

The Builder pattern lets us construct complex objects step by step. Instead of one giant constructor with 10 parameters, we call small, clear methods one at a time.

Think of it like building a custom burger. We start with a bun, add a patty, add cheese, add sauce, add lettuce. Each step is optional. We build exactly what we want, and at the end we get our burger.

The Problem It Solves

Ever seen a constructor like this?

new House(4, 2, true, false, true, "wood", 2, true, false, "red")

What does true mean? What’s 2? Is "red" the roof or the door? This is called the telescoping constructor problem. It’s unreadable and error-prone.

The Builder fixes this:

House.builder().rooms(4).bathrooms(2).hasGarage(true).roofColor("red").build()

Now we can see exactly what each value means.

How It Works

Builder Pattern Flow
Builder
holds partial state
→ .step1() → .step2() → .step3()
.build()
returns final Product
Each step returns this (the builder itself) → enables method chaining

The key ideas:

  1. Each setter method returns this (the builder), so we can chain calls
  2. The build() method at the end assembles and returns the final object
  3. Steps are optional — we only set what we need

Code Implementation

class Pizza:
    def __init__(self):
        self.size = "medium"
        self.cheese = False
        self.pepperoni = False
        self.mushrooms = False
        self.extra_sauce = False

    def __str__(self):
        toppings = []
        if self.cheese: toppings.append("cheese")
        if self.pepperoni: toppings.append("pepperoni")
        if self.mushrooms: toppings.append("mushrooms")
        if self.extra_sauce: toppings.append("extra sauce")
        return f"{self.size} pizza with {', '.join(toppings) or 'nothing'}"

class PizzaBuilder:
    def __init__(self):
        self._pizza = Pizza()

    def size(self, size: str):
        self._pizza.size = size
        return self  # return self for chaining

    def add_cheese(self):
        self._pizza.cheese = True
        return self

    def add_pepperoni(self):
        self._pizza.pepperoni = True
        return self

    def add_mushrooms(self):
        self._pizza.mushrooms = True
        return self

    def add_extra_sauce(self):
        self._pizza.extra_sauce = True
        return self

    def build(self) -> Pizza:
        return self._pizza

# Clean, readable creation
pizza = (PizzaBuilder()
    .size("large")
    .add_cheese()
    .add_pepperoni()
    .add_extra_sauce()
    .build())

print(pizza)  # large pizza with cheese, pepperoni, extra sauce
class Pizza {
  constructor() {
    this.size = "medium";
    this.cheese = false;
    this.pepperoni = false;
    this.mushrooms = false;
    this.extraSauce = false;
  }

  toString() {
    const toppings = [];
    if (this.cheese) toppings.push("cheese");
    if (this.pepperoni) toppings.push("pepperoni");
    if (this.mushrooms) toppings.push("mushrooms");
    if (this.extraSauce) toppings.push("extra sauce");
    return `${this.size} pizza with ${toppings.join(", ") || "nothing"}`;
  }
}

class PizzaBuilder {
  #pizza = new Pizza();

  size(size) {
    this.#pizza.size = size;
    return this; // return this for chaining
  }
  addCheese() { this.#pizza.cheese = true; return this; }
  addPepperoni() { this.#pizza.pepperoni = true; return this; }
  addMushrooms() { this.#pizza.mushrooms = true; return this; }
  addExtraSauce() { this.#pizza.extraSauce = true; return this; }

  build() { return this.#pizza; }
}

// Clean, readable creation
const pizza = new PizzaBuilder()
  .size("large")
  .addCheese()
  .addPepperoni()
  .addExtraSauce()
  .build();

console.log(pizza.toString());
// large pizza with cheese, pepperoni, extra sauce
public class Pizza {
    private String size;
    private boolean cheese;
    private boolean pepperoni;
    private boolean mushrooms;
    private boolean extraSauce;

    // Private constructor -- only the builder can create Pizza
    private Pizza() {}

    @Override
    public String toString() {
        List<String> toppings = new ArrayList<>();
        if (cheese) toppings.add("cheese");
        if (pepperoni) toppings.add("pepperoni");
        if (mushrooms) toppings.add("mushrooms");
        if (extraSauce) toppings.add("extra sauce");
        return size + " pizza with " +
            (toppings.isEmpty() ? "nothing" : String.join(", ", toppings));
    }

    // Static inner Builder class
    public static class Builder {
        private Pizza pizza = new Pizza();

        public Builder size(String size) {
            pizza.size = size;
            return this;
        }
        public Builder addCheese() { pizza.cheese = true; return this; }
        public Builder addPepperoni() { pizza.pepperoni = true; return this; }
        public Builder addMushrooms() { pizza.mushrooms = true; return this; }
        public Builder addExtraSauce() { pizza.extraSauce = true; return this; }

        public Pizza build() { return pizza; }
    }
}

// Usage:
// Pizza pizza = new Pizza.Builder()
//     .size("large")
//     .addCheese()
//     .addPepperoni()
//     .addExtraSauce()
//     .build();

Builder vs Constructor vs Setters

ApproachProsCons
ConstructorSimple, all-at-onceUnreadable with many params
SettersClear namesObject in incomplete state between sets
BuilderReadable, immutable resultMore code to write

The Builder wins when we have 4+ optional parameters. Below that, a constructor is fine.

When to Use

  • Objects with many optional parameters (think HTTP requests, queries, configs)
  • When we want the final object to be immutable (set everything during build, then lock it)
  • When construction has multiple steps that can be combined differently
  • Any time we see a constructor with more than 4-5 parameters

When NOT to Use

  • Simple objects with 2-3 fields — just use a constructor
  • When every field is required — a constructor with named parameters (Python kwargs) works fine

In simple language, Builder is like filling out a form. We fill in what we need, skip what we don’t, and at the end we hit “submit” (build). The result is a clean, fully-constructed object. No guessing what parameter 7 means.