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
this (the builder itself) → enables method chaining
The key ideas:
- Each setter method returns
this(the builder), so we can chain calls - The
build()method at the end assembles and returns the final object - 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
| Approach | Pros | Cons |
|---|---|---|
| Constructor | Simple, all-at-once | Unreadable with many params |
| Setters | Clear names | Object in incomplete state between sets |
| Builder | Readable, immutable result | More 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.