Skip to content
FollowtheDamnCode

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

The client drives named steps; build() returns the finished product
  • Builder — holds partial state, offers one method per step, and returns self so calls chain.
  • Product — the finished, ideally immutable object.
  • Director (optional) — a function that runs a reusable recipe of steps.

In code

A fluent query builder that refuses incomplete queries
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 rows

Run 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

  1. Identify the steps to build the product; make each a method that stores its input and returns self.
  2. Put validation that spans several fields in build().
  3. Have build() return a new product (immutable if you can), so the builder can be reused.
  4. 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

ProsCons
Readable, self-documenting constructionExtra class mirroring the product's fields
No half-built objects escapeEasy to forget build()
One place for cross-field validationOverkill for simple objects

How it relates

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.