skip to content

How do you use a Builder to produce an immutable object, and what role does the build() method play?

level: middleimportance: must knowfreq 64%

answer

  1. Builder mutable, target immutable (private final fields)
  2. build() = validate + default + defensive copy + construct
  3. Private constructor takes the builder
  4. Final fields → safe publication, share without sync
  5. Each build() is an independent frozen snapshot

basics

~10 s

The builder collects values in its own mutable fields, then build() passes them to the target's private constructor, which stores them in final fields. After build() returns, the object can't be changed.

solid answer

~50 s

The builder is the mutable scratchpad; the target object is immutable. You give the target only final fields and a private constructor that takes the builder (or its values). The builder accumulates the configuration through fluent methods, and build() is the single hand-off: it validates invariants (fail fast with an exception if something is wrong), applies any defaults, defensively copies any mutable inputs like collections or Date, and then calls the target's private constructor. Because the target's fields are final and nothing outside can call its constructor, the returned object is fully constructed, consistent, and unmodifiable — safe to share across threads without synchronization. The same builder instance can typically be reused or further mutated to build variants, but each built object is a frozen snapshot. Putting validation in build() rather than in setters means a half-configured builder is fine, but you can never obtain an invalid finished object.

go deeper

for a junior

Knows that build() returns the final object and that the object's fields shouldn't change afterward.

for a middle

Implements a builder that produces an immutable target with final fields, private constructor, and validation in build().

for a senior

Adds defensive copying, explains safe publication / thread-safety of properly constructed immutable objects, and handles builder reuse and aliasing hazards.

for a principal

Designs construction conventions across a codebase, weighs builder vs record vs factory, and reasons about memory-model guarantees and API evolution (adding fields without breaking callers).

## Two objects, two responsibilities The Builder pattern deliberately splits construction into **two** objects: 1. **The builder** — *mutable*. It exists only during construction and holds the values you're accumulating. 2. **The target** — *immutable*. It's the real object you want, and once produced it can never change. Immutability means: all fields are `private final`, there are no setters, the class is `final` (or otherwise not subclassable in a way that breaks the guarantee), and any mutable field is defensively copied. (See *Building Immutable Classes* for the full checklist.) The Builder is how you keep construction *readable* without giving up that immutability. ## The shape ```java public final class HttpRequest { private final URI uri; // required private final String method; // optional, defaults to GET private final Map<String,String> headers; // mutable input → defensively copied private HttpRequest(Builder b) { // private: only Builder can call it this.uri = b.uri; this.method = b.method; this.headers = Map.copyOf(b.headers); // defensive copy → caller can't mutate ours } public static class Builder { private final URI uri; private String method = "GET"; private final Map<String,String> headers = new HashMap<>(); public Builder(URI uri) { this.uri = Objects.requireNonNull(uri); } public Builder method(String m) { this.method = m; return this; } public Builder header(String k, String v) { headers.put(k, v); return this; } public HttpRequest build() { // 1) validate invariants — fail fast if (method.isBlank()) throw new IllegalArgumentException("method required"); // 2) hand the collected state to the immutable target return new HttpRequest(this); } } } ``` ## What `build()` is responsible for `build()` is the **single, controlled hand-off point** from mutable to immutable. Concentrating logic here is the whole reason the pattern is safe: - **Validation / invariant enforcement.** Check cross-field rules (e.g. "end must be after start") and required values. Throwing here means an invalid *finished* object can never escape — the only thing that can be invalid is a builder, which no one else holds. - **Defaulting.** Optional fields that were never set already carry their default value from the builder's field initializer. - **Defensive copying.** If a field is a mutable type (a collection, array, `Date`), copy it during construction so later mutation of the caller's object — or of the builder — can't corrupt the immutable result. (Copying the *other* direction, in getters, is the immutable-class concern.) - **Construction.** Call the target's private constructor. ## Why this yields true immutability and thread-safety Once `build()` returns: - The fields are `final`, so no method can reassign them. - The constructor is `private`, so nobody outside can create an inconsistent instance. - Defensive copies mean shared mutable inputs can't reach in and mutate state. A properly constructed immutable object (with `final` fields) is also **safely published** under the Java Memory Model: other threads that see the reference are guaranteed to see the fully-initialized fields, so it can be shared **without synchronization**. ## Reuse and variants The builder is reusable: you can build, then tweak one field and build again to get a *different* frozen snapshot. Each `build()` produces an independent object. Be aware that if `build()` does **not** copy a mutable collection, two objects built from the same builder could alias the same collection — another reason to defensively copy in the constructor. ## Relation to records A Java **record** is immutable by construction, so for simple immutable data you may not need a builder at all. But a record's canonical constructor is positional, so when a record has many components (especially optional ones) a hand-written builder that ends in `build()` returning the record is still the readable choice.

  • Why defensively copy a collection inside the target's constructor rather than just assigning the builder's reference?
    Otherwise the immutable object and the (mutable) builder — or the caller — share the same collection, so mutating it later silently changes the supposedly immutable object's state.
  • Why put validation in build() instead of in the individual setter methods?
    Cross-field invariants can only be checked once all values are present, and centralizing validation guarantees no invalid finished object can ever be returned even though the builder may pass through partial states.

saying these in an interview costs you the question

  • Exposing public setters on the target — that destroys immutability; setters belong on the builder.
  • Skipping defensive copies of collections/arrays, letting the caller mutate the 'immutable' object's internals.
  • Validating in each fluent setter instead of in build(), which breaks cross-field validation and partial configuration.
  • Leaving the target's constructor public, so callers can bypass the builder and its validation.

context