Builder Pattern
Construct complex objects step by step. A creational design pattern for readable, configurable object construction.
Overview
The Builder Pattern is a creational design pattern that lets you construct complex objects step by step. It separates the construction of an object from its representation, allowing the same construction process to create different representations.
It shines when an object has many optional parameters, nested components, or when you want a fluent, readable API for object creation.
When to Use
Use the Builder Pattern when:
- An object has many optional or nested configuration parameters
- You want to enforce a specific construction sequence
- The constructor would have too many parameters (telescoping constructor problem)
- You need different configurations of the same object type
- You want an immutable object built from a mutable builder
Solution
Python
class Pizza:
def __init__(self, size, cheese=False, pepperoni=False, mushrooms=False):
self.size = size
self.cheese = cheese
self.pepperoni = pepperoni
self.mushrooms = mushrooms
def __str__(self):
toppings = []
if self.cheese: toppings.append("cheese")
if self.pepperoni: toppings.append("pepperoni")
if self.mushrooms: toppings.append("mushrooms")
return f"Pizza({self.size}, {', '.join(toppings) or 'plain'})"
class PizzaBuilder:
def __init__(self, size):
self.size = size
self.cheese = False
self.pepperoni = False
self.mushrooms = False
def add_cheese(self):
self.cheese = True
return self
def add_pepperoni(self):
self.pepperoni = True
return self
def build(self):
return Pizza(self.size, self.cheese, self.pepperoni, self.mushrooms)
# Usage
pizza = PizzaBuilder("large").add_cheese().add_pepperoni().build()
print(pizza) # Pizza(large, cheese, pepperoni)
JavaScript
class Pizza {
constructor(size, cheese, pepperoni, mushrooms) {
this.size = size;
this.cheese = cheese;
this.pepperoni = pepperoni;
this.mushrooms = mushrooms;
}
toString() {
const toppings = [
this.cheese && "cheese",
this.pepperoni && "pepperoni",
this.mushrooms && "mushrooms",
].filter(Boolean);
return `Pizza(${this.size}, ${toppings.join(", ") || "plain"})`;
}
}
class PizzaBuilder {
constructor(size) {
this.size = size;
this.cheese = false;
this.pepperoni = false;
this.mushrooms = false;
}
addCheese() { this.cheese = true; return this; }
addPepperoni() { this.pepperoni = true; return this; }
addMushrooms() { this.mushrooms = true; return this; }
build() { return new Pizza(this.size, this.cheese, this.pepperoni, this.mushrooms); }
}
// Usage
const pizza = new PizzaBuilder("large").addCheese().addPepperoni().build();
console.log(pizza.toString()); // Pizza(large, cheese, pepperoni)
Java
public class Pizza {
private final String size;
private final boolean cheese;
private final boolean pepperoni;
private final boolean mushrooms;
private Pizza(Builder builder) {
this.size = builder.size;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.mushrooms = builder.mushrooms;
}
public static class Builder {
private final String size;
private boolean cheese = false;
private boolean pepperoni = false;
private boolean mushrooms = false;
public Builder(String size) { this.size = size; }
public Builder cheese() { this.cheese = true; return this; }
public Builder pepperoni() { this.pepperoni = true; return this; }
public Builder mushrooms() { this.mushrooms = true; return this; }
public Pizza build() { return new Pizza(this); }
}
@Override
public String toString() {
return "Pizza(" + size + ", cheese=" + cheese + ", pepperoni=" + pepperoni + ")";
}
}
// Usage
Pizza pizza = new Pizza.Builder("large").cheese().pepperoni().build();
System.out.println(pizza);
Explanation
The Builder Pattern separates object assembly into two parts:
- Builder: Accumulates configuration state and knows how to construct the final object
- Product (
Pizza): The immutable or fully-configured object returned bybuild()
By returning self (or this) from each configuration method, you create a fluent interface that reads like a sentence. This eliminates constructors with dozens of parameters.
Variants
| Variant | Use Case | Trade-off |
|---|---|---|
| Fluent Builder | Readable step-by-step construction | Requires mutable builder state |
| Director + Builder | Multiple construction sequences | More classes, but reusable recipes |
| Static Factory Builder | Java’s Class.Builder() pattern | Clean API, but tightly coupled to the product |
What Works
- Return
selffrom each step method to enable method chaining - Make the product immutable after
build()is called - Validate in
build(), not in individual steps, for complete error context - Use a Director when you have common preset configurations (e.g.,
pizzaDirector.makeMargherita()) - Document required vs. optional steps so callers know the minimum valid configuration
Common Mistakes
- Mutable products: Allowing modifications after
build()defeats the purpose - Missing validation: Building an invalid object because validation was skipped
- Overly complex builders: A builder for a simple object with 2 fields is overkill
- State leakage: Reusing a builder instance after
build()without resetting state - Forgetting
return self: Breaking the fluent chain by returningNone/void
Best Practices
-
Validate in
build()only. Defer validation until the final step to provide complete error context with all configuration issues. -
Make the product immutable. Once
build()returns the object, it should not be modifiable. This prevents inconsistent state. -
Document required parameters. Clearly distinguish between required and optional configuration steps in your documentation.
-
Use descriptive method names. Method names should clearly indicate what they configure (e.g.,
withTimeout()vssetTimeout()). -
Provide sensible defaults. Default values reduce the number of required method calls for common use cases.
-
Consider a copy constructor. Allow creating a builder from an existing object to support modification patterns.
-
Handle null gracefully. Decide whether to allow null values or throw exceptions, and be consistent.
-
Thread-safety for shared builders. If builders are reused across threads, ensure they are either thread-safe or not shared.
-
Support serialization. Consider adding methods to serialize/deserialize builder state for persistence.
-
Keep builders focused. A builder should construct one type of object. Don’t add unrelated construction logic.
Frequently Asked Questions
What is the difference between Builder and Factory?
Factory decides which class to instantiate. Builder assembles a single complex object step by step. They solve different problems and can be used together.
Should I use a Builder for every class?
No. Use it when constructors become unwieldy (more than 3-4 optional parameters) or when construction has a meaningful sequence.
Can a Builder produce different product types?
Typically no. A Builder is tightly coupled to one product class. Use Abstract Factory if you need different product families.
How do I handle optional parameters in a Builder?
Provide default values in the builder constructor or use nullable types. Validate that required parameters are set before calling build().
Should I use a Builder for immutable objects?
Yes. Builders are excellent for creating immutable objects. The builder holds mutable state during construction, then produces an immutable product.
How does Builder compare to the Prototype pattern?
Prototype clones existing objects. Builder constructs new objects from scratch. Use Prototype when you have a base object to copy, Builder when constructing from parameters.
Can I use Builder with dependency injection?
Yes. Builders can accept dependencies through their constructor or setter methods. This is useful for complex objects that require services or configurations.
How do I test code that uses Builders?
Test that the builder produces valid objects with the expected configuration. Mock dependencies if the builder requires external services.
Should the Builder be a separate class or a nested static class?
Both approaches work. Nested static classes (Java style) keep the builder close to the product. Separate classes are better when the builder is reused across multiple product types.
How do I handle circular dependencies in Builders?
Avoid circular dependencies in builders. If needed, use lazy initialization or post-construction methods to resolve references after both objects are built.
Related Resources
Factory Pattern
Create objects without specifying the exact class to instantiate. A creational design pattern for flexible object creation.
PatternSingleton Pattern
Ensure a class has only one instance and provide global access to it. A creational design pattern for controlled object creation.
PatternDecorator Pattern
Add new functionality to objects dynamically by wrapping them. A structural design pattern for flexible behavior extension.
PatternAbstract Factory Pattern
Create families of related objects without specifying concrete classes. A creational design pattern for consistent object families.
PatternPrototype Pattern for Object Cloning and Configuration
Create new objects by copying existing ones, allowing pre-configured templates and avoiding subclass explosion when object creation is expensive
PatternPrototype Pattern
Create new objects by copying existing ones. A creational design pattern for cloning and object duplication.