AcademyDesign Patterns
Builder pattern
Replacing a constructor drowning in optional parameters, the extra code it costs for objects simple enough not to need it, and build() handing back the same object twice.
6 min readUpdated 3 exercises
Builder solves a different failure mode than Singleton or the factories: not who builds the object, but how unreadable the constructor call becomes once an object has too many optional parts.
A Builder replaces a constructor with too many optional parameters with a fluent chain of setter-like calls that each return this, ending in a single build() that produces the finished object. Reach for it the moment a constructor call needs a comment to explain which positional argument is which.
The problem it solves
Look at this call:
const pizza = new Pizza('large', true, false, ['olives', 'feta'], null, 'thin', true);Nobody can say what true, false or null mean without opening the class. Now picture the constructor five months later, after two more options were added.
- Positional arguments are unreadable. The call site carries no names, so a swapped pair of booleans compiles and ships.
- Optional parts multiply constructors. The "telescoping constructor" trap: one constructor with 3 arguments, one with 5, one with 7, each for a different combination.
- Half-built objects leak. Setting fields one by one after
newmeans the object exists in an invalid state between the calls.
A Builder gives every option a name, lets you set only the ones you care about, and hands you a valid object only at the end.
Structure
Two parts:
- The builder, which collects settings (each setter returns
this) and validates them. - The product, which is created once, complete, by
build().
class Pizza {
constructor(size, toppings) {
this.size = size;
this.toppings = toppings;
}
}
class PizzaBuilder {
#size = 'medium';
#toppings = [];
size(size) { this.#size = size; return this; }
addTopping(topping) { this.#toppings.push(topping); return this; }
build() { return new Pizza(this.#size, [...this.#toppings]); }
}
// new PizzaBuilder().size('large').addTopping('olives').addTopping('feta').build();Every setter returns this (self in Python). That return value is the entire mechanism that lets the calls chain. Drop it from one method and the chain breaks at exactly that call, which is the most common Builder bug: a method that silently returns undefined or None stays invisible until someone chains through the one call that forgot it.
How a chain reads
The chain reads like a sentence, and every value has a name next to it. Compare it with the positional call above: nothing to memorize, and adding a new option later does not break existing chains.
Where it is used in real systems
| Use case | Why a builder helps |
|---|---|
| Query builders (Knex, Drizzle) | A query has many optional clauses: where, order, limit, join. A chain assembles only the ones you need. |
| HTTP requests | Method, headers, body, timeout and retries, most of them optional. |
| Validation schemas (Zod) | z.string().min(1).max(50).email() is a builder: each call adds a rule and returns a new schema. |
| Test data | A buildUser() helper with sensible defaults and overrides, so a test states only what matters to it. |
| Configuration objects | A rate limiter, a cache or a logger with a dozen optional settings. |
| Emails and documents | Recipients, subject, attachments and a body assembled step by step. |
Java StringBuilder | Appending pieces in a loop, then producing one string at the end. |
You have very likely used two of them today without calling them builders: Zod schemas and query builders.
Builder or an options object
In JavaScript and TypeScript there is a lighter alternative, and interviewers like to ask about it:
interface PizzaOptions {
size?: string;
toppings?: readonly string[];
}
function createPizza({ size = 'medium', toppings = [] }: PizzaOptions = {}): Pizza {
return new Pizza(size, toppings);
}
createPizza({ size: 'large', toppings: ['olives', 'feta'] });Named properties solve the readability problem with much less code. Prefer this when the object is simple and everything can be given at once.
A Builder still wins when:
- Construction happens in steps, possibly across several functions, before the object is complete.
- Validation matters.
build()is one place to check that required parts are present and that options do not contradict each other. - The result must be immutable. The builder is mutable while you configure it, and
build()produces a frozen object. - Some steps are conditional. A chain is easy to split with an
ifbetween two calls.
When not to use it
- Few parameters. A constructor with two or three required arguments does not need a builder.
- Everything is required. The point of a builder is optional parts. If nothing is optional, there is nothing to name.
- An options object is enough. Do not write a class where an object literal with defaults does the job.
What interviewers ask
- "What problem does Builder solve?" Constructors with many optional parameters, and the unreadable, error-prone calls they lead to.
- "How does chaining work?" Every setter returns
this. - "Builder versus a constructor with an options object?" The options object is simpler for one-shot construction. The builder wins for step-by-step assembly and validation at
build(). - "Where do you already use one?" Query builders, Zod schemas, test data helpers.
- "What is the Director?" In the classic pattern, an optional class that knows the order of builder calls for common recipes ("a margherita"). Many codebases skip it.
The mistake to avoid
A Builder whose build() can be called twice and hand back the same mutable object both times. build() should return a fresh, independent instance, otherwise mutating one "finished" object mutates every other copy of it. The same goes for the internal list: copy it ([...this.toppings]) when building, or a later addTopping reaches back into an object you already handed out.
Builder is one naming device among several an object-oriented language gives you for controlling how
newgets called. The mechanism above is the same, syntax aside, whether the language is JavaScript, Python or Java.
Now practice it
1 challenge · 2 quizzesReading this page is the cheap half. These are the exercises that make you use it.
- BuilderProBuild a fluent, chainable API from scratch — the same "every setter returns this" rule this page calls out as the one bug to avoid.ChallengeSolve · EasyChallengeSolve · Easy
- Design Patterns · Creational PatternsTen quick questions across Singleton, Factory Method, Abstract Factory, Builder and Prototype, to check the mental model before this one.QuizTake · EasyQuizTake · Easy
- Design Patterns · Creational Patterns — Series 3ProA second, tougher pass on the same creational family, for once the first set feels easy.QuizTake · EasyQuizTake · Easy
Read next
Singleton pattern
The guarantee that exactly one instance can exist, where it earns its place (database pools, caches, config), the global state it costs, and why it breaks on serverless.
7 min
Prototype pattern
Cloning a configured instance instead of rebuilding from scratch, the per-field copy it costs, nested fields included, and a shallow clone() copying a reference, not a value.
6 min