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 comprehension cost rises at every call site.
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 separation of concerns 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); }}
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 verification cost compounds across every call site updated for a new optional input.
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 maintenance cost reshapes across two objects rather than one.
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 enhancement cost drops because adding an input is one builder method, not N call-site updates.
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 accidental complexity without delivering structural guarantees.