Symptom

A constructor takes many positional arguments, several of which are optional and most of which are easy to swap by accident. Call sites pass null or 0 as placeholders for arguments that don't apply, and reviewers cannot tell from the call site which argument means what. The reader's rises at every call site.

Goal

A step-by-step construction surface where each step is named after what it sets, optional inputs are explicit, and the moment of 'now build it' is a separate, terminal call. The product is immutable once built; partial construction states never leak, supporting between assembly and use.

Before the pattern

const request = new HttpRequest(
'GET',
'/api/users/42',
{ 'Accept': 'application/json', 'Authorization': 'Bearer xyz' },
null,
30000,
3,
true
);

After the pattern

const request = new HttpRequestBuilder('GET', '/api/users/42')
.accept('application/json')
.bearer('xyz')
.timeoutMs(30000)
.retries(3)
.followRedirects(true)
.build();
class HttpRequestBuilder {
constructor(method, url) {
this.spec = { method, url, headers: {}, body: null, timeoutMs: 0, retries: 0, followRedirects: false };
}
accept(mediaType) { this.spec.headers['Accept'] = mediaType; return this; }
bearer(token) { this.spec.headers['Authorization'] = `Bearer ${token}`; return this; }
body(value) { this.spec.body = value; return this; }
timeoutMs(ms) { this.spec.timeoutMs = ms; return this; }
retries(count) { this.spec.retries = count; return this; }
followRedirects(flag) { this.spec.followRedirects = flag; return this; }
build() { return new HttpRequest(this.spec); }
}
Example source: Illustrative example written for this site in the spirit of Design Patterns (Gamma, Helm, Johnson, Vlissides, Addison-Wesley, 1994), chapter 3. The book's running example is a maze builder; this JavaScript adaptation uses HTTP request construction to show the same step-by-step assembly with named, optional inputs and a single terminating build() that produces the immutable product.
Pressure

Adding a new optional input to the constructor breaks every existing call site and forces each to add a placeholder for the new slot. Default values drift between languages, frameworks, and developer assumptions; the constructor signature stops describing what a valid request looks like. The team's compounds across every call site updated for a new optional input.

Tradeoff

The builder is a second object the reader navigates to understand what a fully-constructed product contains. Fluent chains hide the build step's complexity behind method-chaining syntax that some teammates find unidiomatic; debugging a misconfigured builder requires stepping through every chained call. The team's reshapes across two objects rather than one.

Relief

Call sites read as a sequence of named decisions; new optional inputs are additive without breaking existing callers; the immutable product is constructed once and never mutated thereafter. Code review reads top-to-bottom in domain terms, not positional argument terms. The team's drops because adding an input is one builder method, not N call-site updates.

Trap

Builders that allow .build() to be called from any state — including states where required inputs were skipped — defeat the immutability win and replace one bug class (positional confusion) with another (silently-incomplete construction). A builder without 'build() validates the full spec' is a fluent setter chain, not the pattern; the cure adds without delivering structural guarantees.