Builder
The Problem It Solves
// which of these 8 arguments is the max-retry? the timeout? nobody knows:
new HttpClient("https://api", 5000, true, false, 3, "UTF-8", 60, null);
Telescoping constructors fail four ways: argument meaning is invisible at call sites; optional parameters force overload pyramids (2ⁿ combinations); identical adjacent types (int, int) invite transposition bugs that compile fine; and adding a parameter breaks every call. Builder fixes all four by replacing positional construction with named steps.
Mechanics
HttpClient client = HttpClient.builder()
.url("https://api.example.com")
.timeout(Duration.ofSeconds(5))
.maxRetries(3)
.gzip(true)
.build();
Every step names its intent; order is free; omitted optionals fall back to defaults.
public final class HttpClient {
private final String url;
private final Duration timeout;
private final int maxRetries;
private HttpClient(Builder b) { // private ctor — only builder builds
this.url = b.url;
this.timeout = b.timeout;
this.maxRe tries = b.maxRetries;
}
public static Builder builder() { return new Builder(); }
public static final class Builder {
private String url; // required — no default
private Duration timeout = Duration.ofSeconds(30);
private int maxRetries = 0;
public Builder url(String url) { this.url = url; return this; } // return this = fluent chain
public Builder timeout(Duration t){ this.timeout = t; return this; }
public Builder maxRetries(int n) { this.maxRetries = n; return this; }
public HttpClient build() {
if (url == null || url.isBlank())
throw new IllegalStateException("url required"); // validation HERE,
return new HttpClient(this); // once, centrally
}
}
}
Three load-bearing decisions inside: return this enables chaining; the outer constructor is private so no path bypasses validation; validation lives in build() where all fields are final-assembled — invariants checked exactly once on complete state (never on half-built objects).
What You Get
| Property | Why it follows |
|---|---|
| Immutable products | All fields set before construction completes |
| Readable call sites | Each value labeled by its setter name |
| Centralized defaults | In one class, not scattered overloads |
| One-time validation | In build() — see maintaining-invariants |
Real-World Sightings
- JDK:
Stream.Builder,Locale.Builder,HttpRequest.newBuilder(). - Lombok
@Buildergenerates this shape — acceptable boilerplate elimination for data-centric classes. - Kafka/OkHttp/gRPC clients are all fluent builders; configuration surfaces industry-wide converged here.
Trade-offs & Limits
- Boilerplate per class (~1.5× field count lines) — worth it past ~4 fields or with any validation.
- Not a fit for tiny stable-value classes (
Money.of(...)) — factory methods win. - Builder instances are single-use after
build()when product holds derived state; document or reset explicitly.
Interview Framing
- Asked to model config-style objects, writing the builder unprompted scores; stating why validation goes in
build()(complete-state invariant checks) scores higher.
The Original Form
GoF’s builder separates what gets built (a fixed sequence of assembly steps) from how each step is performed (the concrete builder). One construction recipe — the director — run against different builders yields entirely different products: same steps, PDF report vs HTML report.
┌────────────────────┐
client ────────► │ Director │
│ construct(): │
│ b.addHeader() │ fixed recipe — knows the ORDER,
│ b.addBody(items) │ knows nothing about output format
│ b.addFooter() │
└─────────┬──────────┘
│ drives steps on ▼
┌────────────────────┐
│ «interface» Builder│
│ +addHeader() │
│ +addBody(...) │
│ +addFooter() │
│ +getResult() │
└─────△───────△──────┘
│ │
┌────────┴─┐ ┌─┴────────┐
│PdfBuilder│ │HtmlBuilder│
(each step emits its own representation)
The dashed line between director and product is deliberate in the GoF diagram: directors don’t know what they’re building — only the step sequence.
Mechanics
interface ReportBuilder {
void addHeader(String title);
void addBody(List<Line> lines);
void addFooter(String page);
Report result();
}
class Director {
Report construct(ReportBuilder b, String title, List<Line> lines) {
b.addHeader(title); // ordering policy lives HERE,
for (var l : lines) // once, shared by every format
b.addBody(List.of(l));
b.addFooter("confidential");
return b.result();
}
}
new Director().construct(new PdfBuilder(), ...); → PDF report
new Director().construct(new HtmlBuilder(), ...); → HTML report
When the Director Earns Its Keep
| Situation | Verdict |
|---|---|
| Multiple representations from one assembly order | Director valuable |
| Callers assemble ad hoc via fluent chain | Skip director — plain fluent builder is simpler |
| Ordering rules are business logic (“footer always last”) | Director centralizes them |
Honest production guidance: most modern usage is fluent-builder only; the director returns when a shared multi-step recipe genuinely exists.
vs Fluent Builder
| GoF builder + director | Fluent builder | |
|---|---|---|
| Driven by | Director’s fixed recipe | Caller’s chained calls |
| Purpose | Same process → different representations | Readable/validated construction of one type |
| Complexity | Higher (three roles) | Lower |
Real-World Sightings
- Document generation frameworks (Apache POI workbooks): one sheet-filling routine, XLSX/PDF/CSV writers as builders.
- SQL query builders where a dialect-agnostic plan renders to MySQL or Postgres syntax.
- Text parsers building different ASTs from identical traversal order.
Interview Framing
- Explaining that the director encodes ordering invariants (header-before-body) is the understanding marker — it answers “why not let callers chain freely?”
Premium Content
Unlock Builder and all premium lessons with a subscription.
From ₹199.99/year — See plans