Skip to article
ALGORITHMICSPatterns
Patterns6 min read

Builder

Assembling a complicated object step by step — and the validation you can only do at the end.


Construct an HTTP request. It might have a body, or headers, or a timeout, or retries, or none of those. Almost everything is optional and the combinations are open-ended.

What people write first

One constructor with every parameter.

new Request('POST', '/orders', body, headers, 30_000, 3, true, undefined, 'json');

Nobody can read that. Which argument is the timeout? What is true? And the signature grows every time an option is added, breaking every existing call.

The usual escape is telescoping constructors — one per plausible combination — which multiplies rather than solves.

The other escape is an options object, and in TypeScript that is genuinely good:

new Request({method: 'POST', url: '/orders', body, timeout: 30_000});

Named, optional, order-independent. For most objects, stop here. Builder is for the cases this does not cover, and they are narrower than the pattern’s popularity suggests.

The pattern

Accumulate in a separate object; construct once at the end.

class RequestBuilder {
private parts: Partial<RequestSpec> = {};
method(m: Method) { this.parts.method = m; return this; } // ← return this
url(u: string) { this.parts.url = u; return this; }
header(k: string, v: string) {
(this.parts.headers ??= {})[k] = v; // accumulates
return this;
}
timeout(ms: number) { this.parts.timeout = ms; return this; }
build(): Request {
if (!this.parts.url) throw new Error('url is required');
if (this.parts.method === 'GET' && this.parts.body) {
throw new Error('GET cannot have a body'); // ← cross-field rule
}
return new Request(this.parts as RequestSpec);
}
}

Toggle the parts and watch both sides change:

the call

select("*")
  .from("orders")
  .where("status", "paid")
  .orderBy("created_at", "desc")
  .limit(20)
  .build()

what it built

SELECT * FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC
LIMIT 20

Every step returns the builder, so the calls chain. Nothing is constructed until build() — which is what lets the object be validated once, when it is complete, rather than after each setter.

Immutability comes free

Because nothing is constructed until build(), the product can have no setters at all:

class Request {
readonly method: Method;
readonly url: string;
// …all readonly, all assigned once in the constructor
}

This is the strongest argument for the pattern in a language without named arguments. You get readable construction and an object that cannot be mutated afterwards — normally a trade-off.

The other Builder

The catalogue’s version is different from the fluent one above, and it is worth knowing because it solves a different problem.

There, a director knows the construction sequence, and multiple builders produce different representations from it:

function buildDocument(builder: DocBuilder) { // the director: one sequence
builder.title('Report');
builder.paragraph('…');
builder.table(rows);
return builder.result();
}
buildDocument(new HtmlBuilder()); // HTML
buildDocument(new MarkdownBuilder()); // Markdown, same sequence

This is genuinely useful for parsers and serialisers — one traversal, several output formats — and it is what a SAX handler is. Most people saying “builder” mean the fluent version, so it is worth being explicit about which you mean.

When not to

If the object has three fields and no rules between them, a builder is ceremony. The trigger is optionality plus rules: many optional parts, and constraints that only make sense once you can see all of them.

And in TypeScript specifically, check whether an options object plus a Readonly<> type gets you there first. It usually does.