Builder Pattern — Step-by-Step Object Construction
What You'll Learn
You will learn how the Builder pattern tames complex object creation by breaking it into named, readable steps, and how a Director orchestrates those steps for reuse. You will understand the difference between a Builder and a Factory, and when each is appropriate.
Why It Matters
Constructors with ten parameters are error-prone and unreadable. Telescoping constructors scale poorly as optional parameters grow. Builder solves both problems by guiding the caller through a fluent, step-by-step API that makes object creation self-documenting. For example, an HTTP request object with URL, headers, body, query params, timeout, and authentication settings becomes unmanageable as a single constructor. A Builder lets the caller specify only what they need and in any order, while the build() method validates that all required fields are present.
Real-World Use
Query Builders in ORMs construct SQL statements through chained method calls like query.select("name").from("users").where("age > 18"). DodaTech's pipeline editor uses the Builder pattern to construct multi-stage data processing flows where each stage configuration step is validated as you go — the editor won't let you connect an incompatible output to an input, catching errors before the pipeline even runs.
The Pattern
A Director defines the order of construction steps. A Builder interface declares each step. Concrete Builders produce different representations.
from abc import ABC, abstractmethod
class QueryBuilder(ABC):
@abstractmethod
def select(self, columns: list) -> "QueryBuilder":
pass
@abstractmethod
def from_(self, table: str) -> "QueryBuilder":
pass
@abstractmethod
def where(self, condition: str) -> "QueryBuilder":
pass
@abstractmethod
def build(self) -> str:
pass
class PostgresQueryBuilder(QueryBuilder):
def __init__(self):
self._query = []
def select(self, columns: list) -> "PostgresQueryBuilder":
self._query.append(f"SELECT {', '.join(columns)}")
return self
def from_(self, table: str) -> "PostgresQueryBuilder":
self._query.append(f"FROM {table}")
return self
def where(self, condition: str) -> "PostgresQueryBuilder":
self._query.append(f"WHERE {condition}")
return self
def build(self) -> str:
return " ".join(self._query) + ";"
class SQLDirector:
def __init__(self, builder: QueryBuilder):
self._builder = builder
def simple_select(self, table: str):
return self._builder.select(["*"]).from_(table).build()
def filtered_select(self, table: str, condition: str, columns: list):
return self._builder.select(columns).from_(table).where(condition).build()
builder = PostgresQueryBuilder()
director = SQLDirector(builder)
print(director.simple_select("users"))
print(director.filtered_select("users", "age > 18", ["name", "email"]))
SELECT * FROM users;
SELECT name, email FROM users WHERE age > 18;
Structure
classDiagram
class Director {
+construct(builder: Builder)
}
class Builder {
<>
+buildPartA()
+buildPartB()
+getResult(): Product
}
class ConcreteBuilder1 {
+buildPartA()
+buildPartB()
+getResult(): Product1
}
class ConcreteBuilder2 {
+buildPartA()
+buildPartB()
+getResult(): Product2
}
class Product1
class Product2
Director --> Builder : uses
ConcreteBuilder1 ..|> Builder
ConcreteBuilder2 ..|> Builder
ConcreteBuilder1 --> Product1 : produces
ConcreteBuilder2 --> Product2 : produces
Real-World Usage
- Java
StringBuilder— mutable sequence of characters built step-by-step, finalised withtoString(). - Kotlin
buildList { }/buildMap { }— idiomatic Builder DSLs for collections. - Lombok
@<a href="/design-patterns/builder/">Builder</a>— generates Builder classes for Java POJOs at compile time. - React
useReducer— action dispatching follows a Builder-like pattern for constructing state transitions.
Related Patterns
- Abstract Factory creates families of objects; Builder constructs a single complex object.
- Strategy can provide different building steps at runtime.
- Composite often uses Builder to construct tree structures.
Pros and Cons
| Pros | Cons |
|---|---|
| Fine-grained control over construction | Increases code size with Builder classes |
| Reusable construction Process | Requires a Director for standardisation |
| Immutable products after build | Overkill for objects with few fields |
| Fluency improves readability | Builders with mutable state are not thread-safe |
The key insight in the code above is the return self pattern — each Builder method returns the Builder itself, enabling method chaining. The Director encapsulates common construction sequences, so clients don't repeat themselves. Without the Director, each client would manually call .select().from_().build() every time they needed a simple query.
Practice Questions
- Refactor a constructor with 7 parameters into a Builder. What trade-offs emerge in terms of code size versus API clarity?
- How would you enforce that certain build steps must be called before others? Consider a Builder where
from_()must precedewhere(). - Implement a Builder that validates the constructed object before returning it, raising clear error messages for missing required fields.
- When does it make sense to omit the Director and let clients use the Builder directly?
Challenge
Create a second concrete Builder (MySQLQueryBuilder) that produces MySQL-compatible SQL syntax. Use the same SQLDirector with both builders and verify the Director works without modification.
Real-World Task
Find a class in your project with more than five constructor parameters or a complex initialisation sequence. Refactor it using the Builder pattern and use DodaTech's complexity analyser to measure the improvement in code maintainability.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro