Builder
What This Concept Is
Builder is a creational pattern for constructing complex objects in multiple steps, keeping the construction process separate from the finished representation, and giving the caller a readable and safe way to fill in optional or order-sensitive fields.
Two widely-used flavors exist:
- GoF Builder: a director drives a builder through an ordered sequence; several builders can produce different representations.
- Fluent Builder: more common in modern code --
new Request.Builder().url(u).method(M).header(h, v).build()-- where the builder is a mutable staging object whosebuild()returns an immutable product.
The problem it absorbs: an object has too many constructor parameters, too many optional ones, or an invariant that must hold at the end but not during intermediate steps.
Why It Matters Here
Builder is the pattern most likely to be correct even in simple codebases. Anywhere you see a constructor with more than three or four parameters, two or three booleans, or half a dozen overloads, a Builder turns the call site from a line of mystery positional arguments into a self-documenting recipe.
It is also the pattern that most often replaces telescoping constructors and "options objects that could hold anything."
Concrete Example
// Fluent Builder for an HTTP request
public final class HttpRequest {
public final String url;
public final String method;
public final Map<String,String> headers;
public final byte[] body;
public final Duration timeout;
private HttpRequest(Builder b) {
if (b.url == null) throw new IllegalStateException("url required");
this.url = b.url;
this.method = b.method;
this.headers = Map.copyOf(b.headers);
this.body = b.body;
this.timeout = b.timeout;
}
public static Builder builder() { return new Builder(); }
public static final class Builder {
private String url;
private String method = "GET";
private Map<String,String> headers = new HashMap<>();
private byte[] body;
private Duration timeout = Duration.ofSeconds(30);
public Builder url(String u) { this.url = u; return this; }
public Builder method(String m) { this.method = m; return this; }
public Builder header(String k, String v) { headers.put(k, v); return this; }
public Builder body(byte[] b) { this.body = b; return this; }
public Builder timeout(Duration d) { this.timeout = d; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
}
HttpRequest req = HttpRequest.builder()
.url("https://api.example.com/v1/users")
.method("POST")
.header("Authorization", "Bearer " + token)
.body(payload)
.build();
Structural sketch:
Caller --> Builder.step().step().step().build() --> immutable Product
(mutable staging) (validated once)
The built product is immutable; the Builder absorbs the messy stage.
Common Confusion / Misconception
- Builder is not Abstract Factory. Builder produces one complex object from many steps; Abstract Factory produces many simple objects that must be compatible.
- A Builder that only wraps a constructor with two parameters is ceremony. Prefer it when the constructor is big, the invariants are end-of-process, or the object should be immutable.
- In languages with named and default arguments (Kotlin, Python), the language often absorbs the Builder's work; reach for a Builder only when validation or fluency pays extra.
How To Use It
- Identify a constructor that is painful: long parameter lists, lots of optional values, order mistakes.
- Make the product fully immutable and move every setter onto an inner
Builder. - Validate invariants once, inside
build(), not on every setter call. - Keep the Builder scope narrow; resist adding methods that are actually business logic.
Check Yourself
- Why should validation happen inside
build()rather than inside each setter? - What is the readability cost of replacing a 10-argument constructor with a Builder?
- In a language with named parameters, when is a Builder still worth writing?
Mini Drill or Application
Take a codebase constructor with at least six parameters or several booleans. Do three things:
- Convert it to a fluent Builder that produces an immutable product.
- Add one invariant that must be checked at
build()time (e.g.,method == "GET"impliesbody == null). - Compare a before/after call site side by side. Keep the one that is easier to read at 3 a.m.