skip to content

Java 16+ added records, and static factory methods have long existed. When would you still choose a hand-written Builder over a record or a static factory method?

level: seniorimportance: should knowfreq 48%

answer

  1. Records & static factories are still positional → many optionals = same old pain
  2. Builder = named + skippable + validated + immutable
  3. Static factory adds: name, caching, return subtype (Item 1)
  4. Compose: record + static nested Builder + builder() entry point
  5. Few/required → record/factory; many/optional → Builder

basics

~20 s

Records and static factories are great for a few parameters, but they still take all values positionally. Choose a Builder when there are many parameters — especially several optional — so callers can name and skip them and get a readable, validated, immutable object. You can even combine a record with a Builder.

solid answer

~50 s

A record gives you a concise immutable carrier, but its canonical constructor is still positional and requires every component, so with many — especially optional — fields you get the same unreadable, transposition-prone argument list the Builder was invented to fix. A static factory method (of/valueOf/getInstance) improves on a raw constructor with a meaningful name, instance caching, and the freedom to return a subtype, but it too takes a fixed positional parameter list and doesn't gracefully express many optional params. So the Builder still wins precisely when the parameter count is large with several optional or same-typed values: named, skippable parameters; centralized validation in build(); and a single immutable result. These tools compose: you can keep a record (for equals/hashCode/toString) and add a static nested Builder that validates in its compact/canonical constructor, or expose a static factory as the entry point that returns a Builder. Rule of thumb: few params → record or static factory; many/optional → Builder.

code

java · 24 lines
java
// Compose a record (value semantics) with a Builder (named/optional params).
public record Person(String first, String last, String email, int birthYear) {
    public Person {                       // compact constructor: validate/normalize
        if (first == null || last == null)
            throw new IllegalArgumentException("name required");
    }

    public static Builder builder(String first, String last) { // static factory entry
        return new Builder(first, last);
    }

    public static final class Builder {
        private final String first, last;          // required
        private String email = null;               // optional
        private int birthYear = 0;                  // optional
        private Builder(String first, String last) { this.first = first; this.last = last; }
        public Builder email(String e)      { this.email = e; return this; }
        public Builder birthYear(int y)     { this.birthYear = y; return this; }
        public Person build() { return new Person(first, last, email, birthYear); }
    }
}

// usage: named + skip optionals
Person p = Person.builder("Ada", "Lovelace").birthYear(1815).build();

go deeper

for a junior

Knows records are concise immutable data classes and that a Builder helps when there are many parameters; may not yet articulate the positional-vs-named distinction.

for a middle

Explains that records and static factories are positional and required-only, so many optional params still favor a Builder; can pick the right tool for a small vs large parameter set.

for a senior

Lists static-factory advantages (name, caching, subtype), explains precisely why positional construction fails for many optionals, and composes a record with a nested Builder and a builder() entry point with validation in the compact constructor.

for a principal

Frames the choice as an API-design and evolution decision (binary/source compatibility, validation locus, partial construction, instance control), and sets team conventions for when each idiom — and their combinations — applies.

## The three tools **Record** (Java 16+): `record Point(int x, int y) {}` auto-generates a private final field and accessor per component, plus `equals`, `hashCode`, `toString`, and a *canonical constructor* whose parameters are the components in order. Records are immutable carriers for a fixed, named group of values. **Static factory method** (Effective Java Item 1): instead of `new Foo(...)`, a `public static Foo of(...)` (or `valueOf`, `getInstance`, `from`). Benefits over a constructor: it has a *name* (so two factories with the same parameter types can coexist), it can *cache/return a shared instance*, and it can *return any subtype* of its declared return type. But its parameter list is still fixed and positional. **Builder** (Effective Java Item 2): the fluent, named-parameter assembler discussed in this topic. ## Where records and static factories fall short for 'many optional params' Both a record's canonical constructor and a static factory take a **positional parameter list**. With many parameters this reproduces the very problems the Builder solves: - **No named arguments** — Java has none, so `new Person("Ada", "Lovelace", null, null, 1815, null)` is unreadable and easy to transpose between same-typed args (two `String`s, two `null`s). - **No clean way to omit optionals** — every component must be supplied; you pass `null`/`0`/defaults explicitly. You could add overloaded factories per optional combination, but that's just telescoping under a new name. - **Combinatorial overloads** — trying to offer 'only the fields I care about' via many factory overloads explodes as optional fields grow. A record also can't *partially* construct: there's no place to accumulate a subset of fields and finish later. ## Where each tool is the right call - **Few parameters, all (or mostly) required, simple immutable carrier:** a **record** — minimal boilerplate, free `equals`/`hashCode`/`toString`. - **Few parameters but you want a name, caching, or to return a subtype/interface:** a **static factory method**. - **Many parameters, especially several optional or same-typed; readability and validation matter; you expect the list to grow:** a **Builder**. ## They compose — you rarely pick just one These aren't mutually exclusive: - **Record + Builder:** declare the data as a `record` (to get value semantics for free) and add a **static nested `Builder`** plus a `static Builder builder()` entry point. The record's *compact constructor* is a natural home for validation/normalization, and the Builder gives the named, optional-friendly construction path. Callers use `Foo.builder().a(1).c(3).build()`; the Builder calls the record's canonical constructor. - **Static factory as the Builder entry point:** expose `Foo.builder()` (a static factory returning a Builder) instead of `new Foo.Builder()` — combining Item 1 and Item 2. ## Decision summary Reach for a record or static factory by default for small, fixed value groups; switch to (or layer in) a Builder once the parameter list is large or has several optional/same-typed members. The deciding factors are **number of parameters, how many are optional, how easy transposition is, and whether you need centralized validation or partial accumulation** — not the mere availability of records.

  • Why doesn't 'records have a canonical constructor' make the Builder obsolete?
    The canonical constructor is positional and requires every component. With many — especially optional or same-typed — fields it reproduces the unreadable, transposition-prone, can't-skip-optionals problems the Builder exists to fix. Records and Builders compose rather than compete.
  • Name two advantages a static factory method has over a public constructor.
    It has a meaningful name (so multiple factories with identical parameter types can coexist and read clearly), and it can control instances (cache/return a shared instance) or return any subtype of the declared return type — none of which a constructor can do.

A record is a pre-printed form with every blank required in order; a static factory is the same form but with a friendlier title and you can grab a cached copy. A Builder is an interactive order screen where you tick only the options you want and a clerk checks the whole order before finalizing it.

saying these in an interview costs you the question

  • Asserting records make the Builder pattern obsolete — they're positional and don't handle many optional params well.
  • Treating static factory and Builder as the same thing — a factory takes a fixed positional list; a Builder accumulates named params.
  • Thinking you must pick exactly one — record + Builder + a builder() static factory commonly compose.
  • Using a record with 8 positional components including several Strings/optionals and calling it 'clean'.

context