Java 52: The Builder Pattern & Fluent APIs
easy⏱ 5 mincoursejava
Why not telescoping constructors?
With 8 fields (some optional), you either write a dozen overloaded constructors (telescoping — unreadable: new Req(url, GET, null, 30, null, true, ...)) or a setter-based JavaBean (mutable, can be used half-built). The Builder avoids both: each option is a named fluent call, the object is built once and frozen, and you can't observe it in an inconsistent state.
HttpRequest req = HttpRequest.builder("https://api")
.method("POST")
.header("Accept", "json")
.timeout(5000)
.build(); // validated, immutable
Fluent chaining: return this
Each builder method mutates the builder's own state and return this, so calls chain. The final build() runs validation (throw on missing/invalid required fields), applies defaults for unset optionals, and constructs the immutable product by passing the accumulated state to a private constructor. Validation lives in one place — build() — not scattered across setters.
Immutability of the product
The built object exposes only final fields and getters — no setters. The builder, not the product, is mutable. For collection fields, defensively copy on build() (List.copyOf(...)) so later mutation of the caller's list can't leak into the immutable object. A record makes a great immutable product type, with the Builder as its static nested helper.
Build an immutable HttpRequest builder
Create HttpRequestBuilder with fluent method, header(k,v) (accumulates), body, and timeout. build() must: (1) throw if the URL is blank; (2) default method to 'GET' and timeout to 30000 if unset; (3) defensively copy the headers map; (4) return an immutable HttpRequest. Build a valid POST request and print it, then catch the error from building with a blank URL.
Staged builders enforce required fields
A staged (step) builder returns a different interface after each required call, so the compiler forces the order: builder().url(x).method(y).build() — you literally can't call build() until required steps are done. It's more boilerplate but turns runtime validation into compile-time guarantees. Use it for APIs where misuse is costly.
Quiz: Builder vs setters
Why prefer a Builder over a mutable object with setters? The product is immutable and always fully validated — a setter bean can be observed half-initialized and mutated later, defeating thread-safety and invariants. The Builder concentrates validation in build() and yields an object that's safe to share across threads without synchronization.