How does the Builder pattern let you create fully immutable objects with validated invariants, and why is that hard to achieve with a no-arg constructor plus setters?
answer
- mutability moved into the short-lived builder
- private constructor → final fields
- cross-field checks belong in build()
- defensive copy or TOCTOU
- builder mutable, not thread-safe; reuse leaks state
basics
~20 sThe builder holds the values while you fill them in; the real object is created only once, at build(), through a private constructor. So the product needs no setters and can be immutable, and build() is a single place to check rules that involve several fields together.
solid answer
~50 sWith setters, an object is created empty and mutated afterwards: it is observable in a half-built, invalid state, its fields can never be final, and each setter sees only its own field, so cross-field rules like "endDate must be after startDate" have nowhere sound to live. Builder moves the mutability into a short-lived helper. The product's constructor is private and takes all values at once, so every field can be final; the builder accumulates values in its own mutable fields; `build()` runs the full invariant check while no product exists yet, then constructs it. Consequences: an invalid product can never be observed (fail before construction, not after); the product is safe to publish across threads without synchronization; defensive copies of mutable inputs (collections, dates, arrays) are taken inside `build()` or the private constructor so later mutation of the builder cannot reach an already-built product. Note the builder itself is mutable and therefore not thread-safe, and reusing one without resetting leaks state between products.
go deeper
Say the object is created once at build() with a private constructor, so it needs no setters and can be immutable, and that build() is where checks live.
Add the half-built-object window of the setter style, why cross-field invariants have no sound home in setters, and defensive copying of mutable inputs.
Discuss TOCTOU between validation and construction, one-shot vs reusable builders, safe publication of the product, and the builder's own non-thread-safety.
Set the policy: immutable domain objects by default, validation concentrated at construction so no invalid instance ever exists, and awareness that records/data classes with compact constructors or generated with-copy methods may deliver the same guarantee with less code.
### Vocabulary first - **Immutable object** — once created, its observable state never changes. All fields are set in the constructor and never reassigned; there are no setters. - **Invariant** — a rule that must always hold for a valid object, e.g. "quantity ≥ 1", or the cross-field rule "endDate is after startDate". - **Defensive copy** — copying a mutable argument (a list, an array, a mutable date) on the way in, so the caller cannot change your internals afterwards through their own reference. ### Why setters fight all three The no-arg-constructor-plus-setters style creates the object first and fills it later: ``` var b = new Booking(); b.setStart(t1); // <-- here the object exists, is reachable, and is invalid b.setEnd(t2); ``` Three distinct problems: 1. **The half-built window.** Between the constructor and the last setter, the object is reachable. If it is passed to another method, registered in a listener, or (worst) published to another thread inside a constructor or callback, someone can observe an inconsistent state. 2. **No place for cross-field checks.** `setEnd` could compare against `start`, but only if `start` was already set — so validity depends on call order, and `setStart` later can break a rule that `setEnd` already checked. There is no moment the class can point at and say "now I am complete". 3. **Permanent mutability.** Setters must remain public forever, so fields cannot be final, the object is unsafe to share across threads without locking, and it is unsafe as a hash-map key (its hash can change under the map). ### How Builder fixes it mechanically ``` final class Booking { private final Instant start; // final: assigned once private final Instant end; private final List<Guest> guests; private Booking(Builder b) { // private: only the builder can call it this.start = b.start; this.end = b.end; this.guests = List.copyOf(b.guests); // defensive copy } static Builder builder() { return new Builder(); } static final class Builder { private Instant start, end; // mutable, short-lived private List<Guest> guests = new ArrayList<>(); Builder start(Instant v) { this.start = v; return this; } Builder end(Instant v) { this.end = v; return this; } Builder guest(Guest g) { this.guests.add(g); return this; } Booking build() { if (start == null || end == null) throw new IllegalStateException("start and end are required"); if (!end.isAfter(start)) throw new IllegalArgumentException("end must be after start"); if (guests.isEmpty()) throw new IllegalStateException("at least one guest"); return new Booking(this); } } } ``` The mutability has been *relocated*, not removed: the builder is mutable, the product is not. The builder is a local, usually stack-confined, short-lived object, so its mutability is cheap to reason about. ### The subtleties that separate a good answer from a great one **Validate in build(), not in the step methods.** A step method can legitimately reject an obviously bad single value (`quantity(-1)`) for a fast, well-located error. But rules involving more than one field must run in `build()`, because only there is the value set complete. Validating cross-field rules in step methods makes correctness depend on call order. **Check-then-construct vs construct-then-check.** Prefer validating *before* calling the private constructor, or validating *inside* it. The dangerous variant is copying the builder's fields into the product and validating afterwards — and worse, some code validates the *builder* fields and then copies, leaving a gap if the private constructor transforms values. **Defensive copies in both directions.** Copy mutable inputs when the product is created (`List.copyOf(b.guests)`), otherwise the builder can be mutated after `build()` and the change will be visible through the supposedly immutable product — a *time-of-check-to-time-of-use* (TOCTOU) hazard: you validated one list and froze a different one. For the same reason, expose collections from the product as unmodifiable views. **The builder is not thread-safe.** Its fields are plain mutable state. Sharing one builder across threads is a data race. Sharing the *product* is fine and is the whole point. **Builder reuse leaks state.** Calling `build()` twice on the same builder is legal and can be useful (a template you tweak), but if the code assumes a fresh start, values from the first product silently carry into the second. Either document reuse explicitly, reset in `build()`, or make `build()` one-shot by flipping a `built` flag and throwing on a second call. **Language-level alternatives.** Records/data classes with a compact or `init` constructor put validation in one place without a builder; some ecosystems generate `withX()` copy methods on immutable types, which cover the "same object, one field different" case that builders are often (mis)used for.
- If build() copies the builder's list into the product, why is that copy necessary?Without it, the product holds the same list object the builder still owns. Adding to the builder after build() would mutate the 'immutable' product — and would bypass the validation that already ran. The copy makes the product's state independent, which is also why the product should hand out unmodifiable views rather than its internal list.
- Is it safe to call build() twice on the same builder?Mechanically yes, and it is sometimes deliberate — a preconfigured builder used as a template for many similar products. The hazard is accidental reuse: state accumulated for the first product silently appears in the second. If reuse is not intended, make build() one-shot (throw on a second call) or reset the builder; either way, document it.
- Are builders thread-safe?The product is (if genuinely immutable and safely published); the builder is not. It is ordinary mutable state, so it should stay confined to one thread, typically as a local variable.
A form you fill in with a pencil, then submit: the pencil draft can be scribbled on freely, but the moment it is submitted the clerk checks the whole form at once and issues a laminated card that nobody can edit.
saying these in an interview costs you the question
- Putting cross-field validation in individual step methods, making correctness depend on call order
- Storing a caller-supplied mutable collection directly in the product instead of copying it
- Claiming builders make the built object thread-safe *because* of the builder — it is immutability plus safe publication that does that
- Saying Builder removes mutability — it relocates it into a short-lived helper
- Assuming a builder can be shared across threads or freely reused without resetting