Builder and Fluent Builder Pattern
Learning Objectives#
- Diagnose the telescoping constructor problem and explain why optional fields, not object size, are what make direct construction fail.
- Implement a Builder with a private constructor and an immutable product, and enforce invariants at
build()time rather than mid-construction. - Judge when the fluent style is a genuine improvement and when it is a method-chain decoration that made the code worse.
Introduction#
Builder separates the construction of a complex object from its representation, so the same construction process can create different representations. The phrase is more ceremony than the problem needs. The real problem Builder solves is narrow and concrete: constructors with too many parameters, especially optional ones, are unreadable, error-prone, and impossible to validate cleanly.
The pattern is a companion to immutability. If your objects are built once and never change, every field has to be supplied before the object exists, and the set of valid combinations can be large. Builder gives you a mutable staging area (the builder itself) that produces an immutable result.
Problem Statement#
Watch the constructors grow. A Computer starts reasonable and then everything optional arrives:
This is the telescoping constructor anti-pattern, and it fails the moment the field count passes about four. Every call site reads as an unlabeled pile of arguments:
Which argument is the storage? The programmer who wrote the call knows, for about three weeks. Then a new field arrives, say a wifiCard, and now there are five constructors, and the middle ones need default wiring, and someone adds a boolean hasBluetooth and the 2^n explosion of overloads starts feeling real. The deeper problem is that validity checks have nowhere to live. Is storage == 0 invalid? Is a null graphics card allowed for a laptop but not a workstation? The constructor cannot answer, because a constructor signature cannot express policy.
Core Concept#
The Builder pattern replaces the pile of overloads with a mutable staging object that holds defaults, collects values through named methods, and produces the final object in one build() call. The product keeps a private constructor, so the only way in is through the builder.
The calling code becomes self-documenting in a way a constructor never can:
Every named method removes a class of mistakes. You cannot swap the ram and storage arguments anymore because they have names. The defaults live in one place. The invariants live in build(), which is the only point where the object transitions from "being assembled" to "existing," and therefore the only sensible place for validation to run.
Diagram: Builder class structure
One structural note before the fluent part. The GoF Builder has a Director that drives the builder through a fixed construction sequence, which is useful when many products share a procedure. In Java practice the Director is almost always dropped, and the client plays that role by chaining methods directly. The fluent chain you use daily is the pattern minus the director. If your building sequence is identical across many call sites, consider reintroducing it, but for most code the chain is clearer.
The fluent variant#
Fluent builder is the Builder where every setter returns the builder itself, enabling the chain. The trick that makes it ergonomic is that the compile-time type can shrink as you call methods. You can encode "cpu must be set first" by having the first step return a different type that only exposes the remaining methods:
That "staged builder" is overkill for most real objects. It pays off only when the construction order genuinely encodes correctness, for instance when a query builder requires a from before a where. The ordinary fluent builder, where every method returns the same type and you trust the caller to set what matters, covers 95 percent of needs. Do not build the staged version on day one. Build it when the bug reports tell you the order matters.
Real Production Usage#
The JDK ships the most famous one: StringBuilder is the pattern stripped to the bone, a mutable buffer you fill and then toString() once. java.net.URI and javax.ws.rs.core.UriBuilder are fluent builders. Guava's ImmutableMap.builder() and ImmutableList.builder() are the pattern applied to collections that refuse mutation. Spring uses it everywhere: UriComponentsBuilder, WebClient through its RequestHeadersSpec family, and the CriteriaBuilder in JPA. OkHttp's Request.Builder and Retrofit's Retrofit.Builder show the pattern dominating the HTTP client world.
The other common sight is Lombok's @Builder, which generates the builder for you. Using it is not cheating. The pattern is about the shape of construction, not who typed the boilerplate.
Common Mistakes#
Turning every object into a builder because builders look modern. A Point with three coordinates does not need a builder. A builder hides the real constructor behind indirection, and for small objects the direct constructor is clearer. The pattern earns its keep with several optional fields or validation policy, not with a large class.
Mutating the product through the builder after build. If the builder keeps references to collections it handed to the product, callers can mutate the "immutable" object. Copy the data at build() time. The immutable product is the entire point; a builder that leaks internal collections is a trap.
Letting the builder grow validation and defaults until it becomes a second implementation of the class. When the builder starts re-deriving values and coordinating fields, you have two places that know how the product works. Keep the builder dumb: collect fields, apply defaults, validate in build().
Interview Perspective#
Builders are the creational pattern interviewers can safely assume you have touched, because nobody survives a real codebase without meeting one. So the bar moves from "can you describe it" to "do you know why it exists." A weak answer defines it as "a class that builds objects step by step." A strong answer starts from the telescoping constructor, explains that named setter methods fix argument-order errors that no compiler catches, and notes that build() is the only sane home for invariants on an immutable object.
The strongest candidates can say when they would not use a Builder, which is the tell that the pattern was learned through pain rather than a diagram.
Common follow-ups:
- "Why does Builder work so well with immutable objects, specifically?"
- "How would you validate required fields, and why does that not belong in the setters?"
Knowledge Check#
- The
Computergains aboolean hasWififield with a default oftrue. Describe the telescoping-constructor change versus the Builder change, and which class of bug each introduces or removes. - A teammate wants the builder to throw on missing
cpuatbuild()time. Where does the check have to live, and what can it not catch that a staged builder could? - Explain why
StringBuilderis a Builder even though it never validates anything and never returns aBuilderfrom a build method.
Key Takeaways#
- Builder exists for optional fields and validation policy, not for object size, and it pairs naturally with immutability.
- Named setter methods eliminate argument-order errors that constructors cannot catch.
build()is the only point where the object is complete, so it is the only place invariants belong.- The fluent chain is Builder without the Director; reintroduce the Director only when many callers share a construction sequence.
- The JDK, Spring, JPA, and the HTTP client world all run on the pattern, which is why it is worth knowing cold.
What's Next#
The next article is Prototype, the creational pattern that flips the premise of everything so far. Instead of constructing an object from nothing, Prototype constructs new objects by copying an existing one. We will cover when construction is expensive enough to justify copying, why Object.clone() is the sharpest-edged API in the whole JDK, and the difference between shallow and deep copies that trips up almost everyone.