Builder
Construct a complex object step by step, with readable calls instead of a constructor with twelve parameters.
Big idea
A Builder assembles a complex object one named step at a time and hands it over only when you call build() — validated and complete. It replaces constructors with long, easy-to-misorder argument lists, and lets the same steps produce different representations.
The problem it solves
Query("users", ["id", "name"], "age > 18", None, "name", True, 50, 0, False)
What is True? What is the None? Which 0 is the offset? Constructors like this — the telescoping constructor — are unreadable at the call site, easy to get subtly wrong, and every new option adds a parameter to every call. Half-built objects are another risk: set fields one by one on a public object and someone will use it before it's valid.
Think of it like
Ordering at a sandwich counter. You don't shout nine words in a fixed order; you go along the counter — bread, then filling, then salad, then sauce — each step named, any step skippable. Nothing is handed over until you say "that's it", and the person at the till checks it's a real sandwich (no bread-only orders).
Structure
- Builder — holds partial state, offers one method per step, and returns
selfso calls chain. - Product — the finished, ideally immutable object.
- Director (optional) — a function that runs a reusable recipe of steps.
In code
class Query:
def __init__(self, sql):
self.sql = sql # finished and fixed
class QueryBuilder:
def __init__(self, table):
self._table, self._cols, self._where = table, ["*"], []
self._order, self._limit = None, None
def select(self, *cols):
self._cols = list(cols)
return self # returning self makes calls chain
def where(self, condition):
self._where.append(condition)
return self
def order_by(self, col):
self._order = col
return self
def limit(self, n):
if n <= 0:
raise ValueError("limit must be positive")
self._limit = n
return self
def build(self):
if self._limit and not self._order:
raise ValueError("a LIMIT without ORDER BY returns arbitrary rows")
sql = f"SELECT {', '.join(self._cols)} FROM {self._table}"
if self._where:
sql += " WHERE " + " AND ".join(self._where)
if self._order:
sql += f" ORDER BY {self._order}"
if self._limit:
sql += f" LIMIT {self._limit}"
return Query(sql)
def recent_active_users(builder): # a "director": a reusable recipe
return builder.select("id", "name").where("active = 1").order_by("last_seen DESC").limit(10).build()
print(QueryBuilder("users").select("id", "name").where("age > 18").order_by("name").build().sql)
print(recent_active_users(QueryBuilder("users")).sql)
try:
QueryBuilder("orders").limit(5).build()
except ValueError as err:
print("refused:", err)output
SELECT id, name FROM users WHERE age > 18 ORDER BY name
SELECT id, name FROM users WHERE active = 1 ORDER BY last_seen DESC LIMIT 10
refused: a LIMIT without ORDER BY returns arbitrary rowsRun and edit it in the app.
Each call reads like a sentence, optional parts are simply left out, and build() is the one place cross-field rules are enforced.
Watch it
How to implement it
- Identify the steps to build the product; make each a method that stores its input and returns
self. - Put validation that spans several fields in
build(). - Have
build()return a new product (immutable if you can), so the builder can be reused. - Optionally capture common recipes in a director function.
In Python, keyword arguments with defaults solve the simple case — Query("users", limit=10, order="name") is already readable. Reach for a builder when construction has ordering, many optional parts, cross-field validation, or several output formats from the same steps.
When is Builder clearly better than a constructor with keyword arguments?
When to use it — and when not to
Use it for objects with many optional parts (HTTP requests, queries, documents, test fixtures), when the same steps should produce different results (an HTML builder and a Markdown builder fed the same steps), or to make sure an object is never seen half-built. Don't add one for a three-field record.
Pros and cons
| Pros | Cons |
|---|---|
| Readable, self-documenting construction | Extra class mirroring the product's fields |
| No half-built objects escape | Easy to forget build() |
| One place for cross-field validation | Overkill for simple objects |
How it relates
- Abstract Factory returns finished parts immediately; Builder assembles one product over several calls.
- Builders often produce Composite trees (a document of sections of paragraphs).
- The Introduce Parameter Object refactoring (Simplifying Method Calls) and the Long Parameter List smell point towards it.
Why do builder step methods usually return self?
Key terms
- Builder — An object that collects construction steps and produces the finished product on build().
- Telescoping constructor — A constructor with a long, growing list of positional parameters.
- Fluent interface — Methods that return the object itself so calls can be chained.
- Director — A reusable recipe that drives a builder through a fixed sequence of steps.