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 | Predefined construction sequences | More classes, but reusable recipes |
| Static Factory Builder | Java’s Class.Builder() pattern | Clean API, but tightly coupled to the product |
Builder vs. Simpler Alternatives
Not every object needs a builder. Use this comparison to decide:
| Approach | When it wins | Why not a Builder |
|---|---|---|
| Plain constructor | Few parameters, all required | Chaining adds boilerplate you don’t need |
| Telescoping constructor | A small set of fixed option combinations | Explodes combinatorially; argument order is error-prone |
| Object literal / spread | Flat options bag with no invariants | No validation, defaults, or invariants enforced at construction |
| Factory method | One-off creation logic | Doesn’t compose options step by step |
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 need 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
Advanced Techniques
Builder with validation and defaults
Add validation in the build() method and provide sensible defaults:
class Pizza:
def __init__(self, size, cheese=False, pepperoni=False, mushrooms=False, sauce="tomato"):
self.size = size
self.cheese = cheese
self.pepperoni = pepperoni
self.mushrooms = mushrooms
self.sauce = sauce
class PizzaBuilder:
def __init__(self, size="medium"):
self.size = size
self.cheese = False
self.pepperoni = False
self.mushrooms = False
self.sauce = "tomato" # Default value
def add_cheese(self):
self.cheese = True
return self
def add_pepperoni(self):
self.pepperoni = True
return self
def set_sauce(self, sauce):
valid_sauces = ["tomato", "bbq", "pesto"]
if sauce not in valid_sauces:
raise ValueError(f"Invalid sauce: {sauce}. Must be one of {valid_sauces}")
self.sauce = sauce
return self
def build(self):
if not self.size:
raise ValueError("Size is required")
if self.size not in ["small", "medium", "large"]:
raise ValueError(f"Invalid size: {self.size}")
return Pizza(self.size, self.cheese, self.pepperoni, self.mushrooms, self.sauce)
Builder for nested objects
Handle complex object graphs with nested builders:
public class House {
private final String address;
private final Kitchen kitchen;
private final List<Room> rooms;
private House(Builder builder) {
this.address = builder.address;
this.kitchen = builder.kitchen;
this.rooms = builder.rooms;
}
public static class Builder {
private String address;
private Kitchen kitchen;
private List<Room> rooms = new ArrayList<>();
public Builder address(String address) {
this.address = address;
return this;
}
public Builder kitchen(Kitchen.Builder kitchenBuilder) {
this.kitchen = kitchenBuilder.build();
return this;
}
public Builder addRoom(Room.Builder roomBuilder) {
this.rooms.add(roomBuilder.build());
return this;
}
public House build() {
return new House(this);
}
}
}
// Usage
House house = new House.Builder()
.address("123 Main St")
.kitchen(new Kitchen.Builder().size("large").build())
.addRoom(new Room.Builder().type("bedroom").size("medium").build())
.addRoom(new Room.Builder().type("bathroom").size("small").build())
.build();
Builder with copy methods
Support creating a builder from an existing object to build modified copies:
class Pizza {
constructor(size, cheese, pepperoni, mushrooms) {
this.size = size;
this.cheese = cheese;
this.pepperoni = pepperoni;
this.mushrooms = mushrooms;
}
toBuilder() {
const builder = new PizzaBuilder(this.size);
if (this.cheese) builder.addCheese();
if (this.pepperoni) builder.addPepperoni();
if (this.mushrooms) builder.addMushrooms();
return builder;
}
}
// Usage: modify an existing pizza without rewriting its config
const originalPizza = new PizzaBuilder("large").addCheese().build();
const modifiedPizza = originalPizza.toBuilder().addPepperoni().build();
Conditional construction
Builders pair well with conditional logic that applies options only when needed. Keep accumulated state in private fields so attribute names never collide with method names:
class QueryBuilder:
def __init__(self):
self.conditions = []
self.joins = []
self._order_by = None
self._limit = None
def where(self, condition):
self.conditions.append(condition)
return self
def join(self, table, on_clause):
self.joins.append((table, on_clause))
return self
def order_by(self, field, direction="ASC"):
self._order_by = (field, direction)
return self
def limit(self, count):
self._limit = count
return self
def build(self):
query = "SELECT * FROM items"
if self.joins:
for table, on_clause in self.joins:
query += f" JOIN {table} ON {on_clause}"
if self.conditions:
query += " WHERE " + " AND ".join(self.conditions)
if self._order_by:
query += f" ORDER BY {self._order_by[0]} {self._order_by[1]}"
if self._limit:
query += f" LIMIT {self._limit}"
return query
# Usage with conditional logic
builder = QueryBuilder()
builder.join("users", "items.user_id = users.id")
if include_active_only:
builder.where("users.active = true")
if sort_by_date:
builder.order_by("created_at", "DESC")
if max_results:
builder.limit(max_results)
query = builder.build()
Building multiple product types
One builder can share configuration across several related products:
public class ReportBuilder {
private String title;
private String author;
private String date;
private List<Section> sections = new ArrayList<>();
public ReportBuilder title(String title) {
this.title = title;
return this;
}
public ReportBuilder author(String author) {
this.author = author;
return this;
}
public ReportBuilder date(String date) {
this.date = date;
return this;
}
public ReportBuilder addSection(Section section) {
this.sections.add(section);
return this;
}
public PDFReport buildPDF() {
return new PDFReport(title, author, date, sections);
}
public HTMLReport buildHTML() {
return new HTMLReport(title, author, date, sections);
}
public MarkdownReport buildMarkdown() {
return new MarkdownReport(title, author, date, sections);
}
}
// Usage: build multiple formats from the same configuration
ReportBuilder builder = new ReportBuilder()
.title("Q4 Sales Report")
.author("John Doe")
.date("2026-01-15")
.addSection(new Section("Executive Summary", "..."))
.addSection(new Section("Data Analysis", "..."));
PDFReport pdf = builder.buildPDF();
HTMLReport html = builder.buildHTML();
MarkdownReport markdown = builder.buildMarkdown();
Building configuration objects
Configuration objects are one of the most common real-world uses of the Builder pattern. A database or HTTP client config typically has many optional fields with defaults — exactly what the telescoping-constructor problem describes:
class DatabaseConfig {
constructor(
public readonly host: string,
public readonly port: number,
public readonly database: string,
public readonly username: string,
public readonly password: string,
public readonly poolSize: number,
public readonly timeout: number,
public readonly ssl: boolean,
public readonly retryAttempts: number
) {}
}
class DatabaseConfigBuilder {
private host: string = 'localhost';
private port: number = 5432;
private database!: string;
private username!: string;
private password!: string;
private poolSize: number = 10;
private timeout: number = 5000;
private ssl: boolean = false;
private retryAttempts: number = 3;
setHost(host: string): this { this.host = host; return this; }
setPort(port: number): this { this.port = port; return this; }
setDatabase(database: string): this { this.database = database; return this; }
setCredentials(username: string, password: string): this {
this.username = username;
this.password = password;
return this;
}
setPoolSize(size: number): this { this.poolSize = size; return this; }
setTimeout(timeout: number): this { this.timeout = timeout; return this; }
enableSsl(): this { this.ssl = true; return this; }
setRetryAttempts(attempts: number): this { this.retryAttempts = attempts; return this; }
build(): DatabaseConfig {
if (!this.database) throw new Error('Database name is required');
if (!this.username || !this.password) {
throw new Error('Credentials are required');
}
return new DatabaseConfig(
this.host, this.port, this.database, this.username,
this.password, this.poolSize, this.timeout, this.ssl, this.retryAttempts
);
}
}
// Production and development presets share the same builder
const prodConfig = new DatabaseConfigBuilder()
.setHost('db.production.com')
.setDatabase('myapp')
.setCredentials('user', 'secret')
.setPoolSize(50)
.enableSsl()
.setTimeout(10000)
.build();
const devConfig = new DatabaseConfigBuilder()
.setDatabase('myapp_dev')
.setCredentials('dev', 'dev')
.build(); // localhost:5432, pool of 10, SSL off
Two practices matter for configuration builders. Define environment presets (development, staging, production) as functions that return a pre-configured builder, so each environment only overrides what differs. And never hardcode secrets in builder calls — load them from environment variables or a secrets manager before passing them in.
Best Practices
-
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’re either thread-safe or not shared.
-
Keep builders focused. A builder should construct one type of object. Don’t add unrelated construction logic.
See Also
- Builder — Refactoring.Guru: intent, structure, and examples in several languages.
- Builder pattern — Wikipedia: formal GoF definition and history.
- Design Patterns (GoF book): the original catalog where the pattern was published.
Frequently Asked Questions
What is the difference between Builder and Factory?
Factory decides which class to instantiate; Builder assembles one complex object step by step. They answer different questions — "which object to create" vs "how to assemble it" — and often appear together.
Should I use a Builder for every class?
No. Past 3-4 optional parameters, or when construction order starts to matter, a builder pays for itself; below that it's mostly boilerplate.
How does Builder compare to the Prototype pattern?
Prototype clones an existing instance; Builder assembles a new one from parameters. Pick Prototype when a template object already exists, Builder when you're constructing from scratch.
Should I use a Builder for immutable objects?
It's one of the best fits — the builder carries the mutable state during construction, then hands you an immutable product.
Can Builders be used for configuration objects?
That's the classic use; see the configuration example in Advanced Techniques above. Optional settings, defaults, and per-environment presets all benefit from it.
How do I handle optional parameters in a Builder?
Give them defaults in the builder's fields or constructor, and check inside build() that the required ones were actually set.
How do I handle validation errors in a Builder?
Collect every problem you find in build() and throw one exception listing them all — callers fix everything in a single pass instead of one error at a time.
How do I handle builder state reset after build()?
The cheap answer is not reusing builders at all — a fresh instance per object costs almost nothing. If you must reuse one, add a reset() method and document it clearly.
Should the Builder be a separate class or a nested static class?
Nested static classes (the Java convention) keep builder and product together; go with a separate class when one builder serves several product types.
Can I use Builder with dependency injection?
Sure — pass collaborators through the builder's constructor or setters like you would for any other object. It's handy when the product needs services during construction.
Related Resources
Factory Pattern
Create objects without specifying the exact class to instantiate. A creational design pattern for flexible object creation.
PatternSingleton Pattern: Java, Python & TypeScript Examples
Ensure a class has only one instance and provide global access to it — the creational design pattern for controlled object creation, with production-ready implementations.
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: Clone Objects Instead of Rebuilding
Create objects by cloning pre-configured instances instead of rebuilding them from scratch. Prototype pattern with Python, Java, and JavaScript examples.