When is the Builder pattern the wrong choice, and what alternatives deliver the same benefits with less machinery?
answer
- duplicated field list drifts silently
- named args + defaults make builders redundant
- required check drops to runtime
- reused builder leaks state; make build() one-shot
- 18 fields = missing decomposition, not a missing builder
basics
~20 sSkip Builder when a class has few, mostly required fields, or when the language already has named arguments with defaults. Builders duplicate the field list, hide missing values until runtime, and a reused builder can leak state from one product into the next.
solid answer
~60 sBuilder buys readability, optionality and immutability at the price of a parallel class that must track every field. It is the wrong tool when: the class has two or three mandatory fields (a constructor is clearer); the language offers named arguments with defaults, records/data classes, or generated copy-with functions, which cover the readability and immutability goals directly; the object is genuinely mutable by nature and callers expect to change it later; or the class has so many fields that a builder is masking a missing decomposition — nested value objects or a parameter object often fix the real problem. Concrete hazards: required fields degrade from compile-time to runtime errors unless you design for it; builders are mutable and not thread-safe, and reusing one without resetting silently carries state into the next product; the duplicated field list drifts, so a new field silently defaults instead of being supplied; deep builder chains for nested structures become unreadable. Prefer generated builders over hand-written ones, and treat a builder as an API decision — once public, its steps are a compatibility surface.
go deeper
Say builders are unnecessary for small classes with few required fields, and that they mean writing and maintaining extra code.
Add named arguments with defaults, records/data classes and static factory methods as alternatives, and mention the duplicated field list.
Cover the loss of compile-time required-parameter checking, builder reuse/state leakage, thread-safety of the builder itself, and defensive copying.
Frame it as API and modelling stewardship: builders are a public compatibility surface, many fields usually signal a missing decomposition into value objects, and language features or configuration binders may make the pattern redundant — set the team's threshold explicitly.
### What Builder actually costs 1. **A duplicated field list.** Every field appears in the product and again in the builder. Adding one means editing two places; forgetting the builder means callers *cannot* set it, while forgetting to pass a builder field into the constructor means it silently defaults — a bug the compiler will not catch. Generated builders (annotation processors, macros, IDE templates) neutralise most of this and are strongly preferred over hand-written ones for large classes. 2. **Downgraded compile-time safety.** A constructor makes required parameters unavoidable; a naive builder makes everything look optional, moving "you forgot the ID" from compile time to whenever that path executes. 3. **Mutable, non-thread-safe helper.** The builder is plain mutable state; sharing one across threads is a data race. It should stay a local variable. 4. **Reuse leaks state.** `build()` twice on one builder yields two products, the second silently inheriting everything set for the first. This is genuinely useful when intended (a template) and a nasty aliasing bug when not. Fix by resetting, or by making `build()` one-shot and throwing on the second call. 5. **Indirection tax.** More types, more navigation, noisier stack traces and diffs. 6. **API commitment.** Once a builder is public, its step names, their order (if staged) and defaults are a compatibility surface. Removing or renaming a step, or changing a default, breaks callers as surely as changing a constructor signature would. ### Alternatives, and when each wins **Named arguments with default values** (Python, Kotlin, C#, Swift, Ruby keyword args, TypeScript object literals). `createUser(name = "a", email = "b", admin = false)` gives the readability, the optionality and the compile-time required check with zero extra code. This is why builders are far more idiomatic in languages that lack the feature. Caveat: in some languages defaults are baked into call sites at compile time, so changing a default requires recompiling callers — a real concern for published libraries. **Records / data classes / immutable value types with a copy-with function.** A record gives immutability and a canonical constructor; a compact/`init` constructor gives single-point validation. `copy(field = x)` or generated `withField(x)` methods cover the common "same object, one thing different" case that builders are often misused for. **A parameter object.** When a group of parameters always travel together (`from`, `to`, `currency`), extracting them into a small value type shortens every signature and often makes the builder unnecessary. If your class needs a builder because it has 18 fields, the honest diagnosis is usually a missing decomposition into 3–4 cohesive value objects, each of which is simple to construct. **Static factory methods with meaningful names.** `Duration.ofSeconds(30)`, `Money.usd(5)`, `Report.blank()` communicate intent better than any chain and cost nothing. For a handful of common configurations, several named factories beat one builder. **Prototype / preconfigured template.** If most instances differ from a baseline by one or two values, clone a configured instance (or share a preconfigured builder as a template, or use copy-with) instead of re-specifying everything. **Deserialization / configuration binding.** For objects assembled from external configuration or JSON, a binder that maps a document onto an immutable type — with schema-level required/optional and validation — often replaces the builder wholesale. ### A decision heuristic Reach for Builder when **two or more** of these hold: more than roughly four parameters; several of them optional; the product must be immutable; validation spans fields; construction happens across code (values gathered in loops or layers); or the same construction process must yield different representations. If only one holds, a simpler tool almost always wins. ### Design guidance if you do build one - Take genuinely required values in the builder's constructor/factory; validate the rest in `build()` and report *all* problems at once. - Copy mutable inputs defensively so the product cannot be mutated through the builder afterwards. - Decide and document whether the builder is reusable; if not, make `build()` one-shot. - Consider an `toBuilder()` on the product for the "copy with changes" case, so callers stop hand-copying fields. - Generate the builder if the class is large; keep the product the source of truth for the field list. - Treat step names and defaults as public API from day one.
- A class has 18 fields and its builder is unwieldy. What is the more likely root cause?Missing decomposition. Eighteen fields usually cluster into three or four cohesive value objects (address, pricing, schedule), each trivially constructible. Extracting them shrinks every signature and often removes the need for a builder entirely — and it improves the model, not just the construction ergonomics.
- Why do builders show up far more in Java than in Kotlin, Python or C#?Those languages have named arguments with default values, which deliver the readability, optionality and compile-time required-parameter check directly at the call site. Builders remain useful there for staged construction, values accumulated across code, or when the same process must produce different representations — but the everyday 'many optional parameters' case is already solved.
- How can a public builder break callers as it evolves?Its step names, their required order in a staged design, and its default values are all part of the contract. Renaming or removing a step is a source break; silently changing a default changes behaviour for every existing caller; and in languages where default arguments are inlined at the call site, changing a default may not even take effect until callers recompile.
Builder is scaffolding: essential for a cathedral, absurd around a garden shed — and scaffolding that outlives the building becomes the thing you maintain.
saying these in an interview costs you the question
- Applying Builder to every multi-field class as a default style rule
- Ignoring that a reused builder carries state into the next product
- Hand-writing large builders instead of generating them, then letting the field lists drift
- Treating a builder as a fix for a class that is really missing a decomposition
- Assuming a builder's step names and defaults can be changed freely after publication