How does the Builder idiom produce an immutable object, and why is calling validation in build() better than validating in individual setter-style methods?
answer
- Target = final class, private final fields, private ctor taking the Builder
- Builder mutable; target immutable; assigned once
- Validate in build()/private ctor = all params known = cross-field invariants
- Copy first, then validate the object's fields (not the builder's)
- Defensive-copy mutable components
basics
~20 sThe target class has all-final fields and a private constructor that copies values from the builder once, so once built it can't change — that's immutable. Validating in build() means the whole object's invariants are checked together, right before it's created, instead of in isolated method calls.
solid answer
~50 sThe target object's fields are private and final and are assigned exactly once inside a single private constructor that takes the Builder and copies its accumulated values; because no setters are exposed and nothing mutates the state afterward, the object is immutable. The Builder, by contrast, holds plain mutable fields you fill in via chained methods. Validation belongs in build() (or the private constructor it calls) rather than in each chaining method because that is the only point where the full set of parameters is known: you can enforce cross-field invariants (e.g. start < end), throw IllegalArgumentException/IllegalStateException before any object escapes, and guarantee that no partially-validated object ever exists. A subtle point: to be robust against a caller mutating the Builder between checking and constructing, copy parameters from the Builder to the object first and validate the object's fields, throwing if a check fails.
code
java · 17 linespublic NutritionFacts build() {
return new NutritionFacts(this);
}
private NutritionFacts(Builder b) {
// 1) copy first so validation sees the object's own (now-fixed) values
this.servingSize = b.servingSize;
this.servings = b.servings;
this.calories = b.calories;
// 2) validate cross-field / range invariants in one place
if (servingSize <= 0)
throw new IllegalArgumentException("servingSize must be > 0");
if (servings <= 0)
throw new IllegalArgumentException("servings must be > 0");
if (calories < 0)
throw new IllegalArgumentException("calories must be >= 0");
}go deeper
Knows the result has no setters and final fields, so it can't change after build(). Can point out that build() creates the object.
Explains that a private constructor copies builder fields into final fields once, and that validation should live in build() because that's when all parameters are known. Can write the validating constructor.
Adds the copy-then-validate ordering to defend against builder mutation, distinguishes IllegalArgumentException vs IllegalStateException, and handles defensive copies of mutable components.
Reasons about the full immutability contract (final class, no leaks, defensive copy in and out), thread-safety/publication guarantees, and how to keep invariants centralized as the class evolves; weighs this against records for simple immutable carriers.
## What 'immutable' means here An **immutable** object is one whose observable state cannot change after construction. In Java you achieve this by: making the class effectively un-subclassable (e.g. `final`), making every field `private final`, exposing **no** mutators (no setters), assigning every field exactly once during construction, and defensively copying any mutable inputs (like arrays or collections) so external code cannot reach in and change them. Immutable objects are inherently thread-safe and freely shareable. ## How the Builder yields immutability The Builder is itself a *mutable* helper — its fields start at defaults and you overwrite them through chained methods like `.calories(100)`. The target class stays immutable because the **only** way to create it is a *private* constructor that takes the finished Builder and copies its fields into the target's `final` fields, once: ```java private NutritionFacts(Builder b) { this.servingSize = b.servingSize; // each final field assigned exactly once this.servings = b.servings; this.calories = b.calories; } ``` Because the constructor is private, no outside code can build the object except through the Builder; because the fields are `final` with no setters, the object never changes after `build()` returns. The Builder is discarded (eligible for GC) once you have the object. ## Why validate in build(), not in the chaining methods The chaining methods (e.g. `calories(int v)`) usually do nothing but store the value and `return this`. You *could* validate inside each one, but that only checks one field in isolation. **Cross-field invariants** — rules that involve more than one parameter, like `servingSize > 0`, `start <= end`, or 'if discount is set, price must be set' — can only be checked once *all* parameters are present, which is at `build()` time. Validating there means: - The full object is checked as a unit, in one place, just before it is created. - If a check fails you throw (`IllegalArgumentException` for a bad argument value, `IllegalStateException` for an illegal combination) and **no invalid object ever escapes** to callers. - You avoid scattering validation logic across many methods. ## The check-then-construct ordering subtlety A Builder is a normal mutable object, so in principle a caller could hold a reference and change it after you validate but before you copy. The robust recipe (from Effective Java) is: in the private constructor, **first copy the Builder's parameters into the object's fields, then validate the object's own fields** (not the Builder's). That way the values you validate are exactly the values the object will keep, immune to later Builder mutation. If a check fails, throw an `IllegalArgumentException` whose message points at the offending parameter. ## Mutable components inside the object If one of the parameters is itself mutable (say a `List` or array), copying the reference is not enough — the caller still holds the original and could mutate it. Make a **defensive copy** when transferring it into the immutable object (and, if you expose it, return an unmodifiable view or another copy). This keeps the 'cannot change after construction' guarantee truly intact.
- Which exception should build() throw on an invalid parameter value?IllegalArgumentException for a bad single value (e.g. negative when positive is required); IllegalStateException for an illegal combination of otherwise-valid values, or when build() is called in a state it shouldn't be. Always include a message naming the offending parameter.
- If a builder parameter is a mutable List, what must build() do to keep the object immutable?Make a defensive copy (e.g. List.copyOf or new ArrayList<>(src)) when transferring it into the object, so the caller's reference can't mutate the stored data; and return an unmodifiable view or a copy from any getter.
The Builder is a draft document you scribble on; build() is signing and notarizing it. Once notarized you get a sealed copy you can't edit, and the notary checks the whole document for consistency at signing time — not each sentence as you write it.
saying these in an interview costs you the question
- Believing the Builder itself is immutable — it's the mutable accumulator; the target is immutable.
- Validating only inside each chaining method, missing cross-field invariants only knowable at build().
- Validating the builder's fields and then copying — a caller could mutate the builder in between; copy first, validate the object's fields.
- Storing a mutable List/array reference directly, leaving the 'immutable' object mutable through the caller's reference.