Builder Pattern
The Builder pattern is a creational pattern that separates the construction of a complex object from its representation, so the same step-by-step process can produce different results. In day-to-day engineering its most common job is narrower and more practical than the textbook definition suggests: it lets you assemble an object that has many parameters — especially many optional ones — without a forest of overloaded constructors and without forcing callers to memorize positional argument order.
The problem it solves
Imagine a Pizza with a size, a crust, and a variable list of toppings. With plain constructors you either write one giant constructor and pass null for everything you don't care about, or you write a combinatorial pile of overloads. Both are error-prone: new Pizza("Large", "Thin", null) tells the reader nothing about which argument is which, and two String parameters in the wrong order compile happily and fail silently.
A builder turns that into a readable, self-documenting chain where every value is labeled by the method that sets it, and where the object is only materialized once, in a final build() call, after which it can be made immutable.
The fluent (modern) builder
This is the form you will write 95% of the time: a static nested Builder whose setters return this so calls chain, and a single build() that hands back an immutable product. There is no separate director — the caller drives the steps directly.
JDK baseline: the Java sample below uses List.of and List.copyOf, which require JDK 9 or later. On JDK 8, substitute Collections.unmodifiableList(new ArrayList<>(toppings)) and an empty new ArrayList<>() default.
// Java (JDK 9+)
public final class Pizza {
private final String size;
private final String crust;
private final List<String> toppings; // immutable
private Pizza(Builder b) {
this.size = b.size;
this.crust = b.crust;
this.toppings = List.copyOf(b.toppings); // defensive, unmodifiable copy
}
@Override public String toString() {
return "Pizza{size=" + size + ", crust=" + crust + ", toppings=" + toppings + "}";
}
public static final class Builder {
// sensible defaults for optional fields
private String size = "Medium";
private String crust = "Hand-tossed";
private List<String> toppings = new ArrayList<>();
public Builder size(String size) { this.size = size; return this; }
public Builder crust(String crust) { this.crust = crust; return this; }
public Builder addTopping(String t) { this.toppings.add(t); return this; }
public Pizza build() { return new Pizza(this); }
}
}
// Usage
Pizza hawaiian = new Pizza.Builder()
.size("Large")
.crust("Thin")
.addTopping("Ham")
.addTopping("Pineapple")
.build();
// Pizza{size=Large, crust=Thin, toppings=[Ham, Pineapple]}
Pizza plain = new Pizza.Builder().build();
// Pizza{size=Medium, crust=Hand-tossed, toppings=[]}The same shape in Go uses a pointer receiver that returns the builder for chaining. Go has no constructors, so the builder is the idiomatic way to express defaults plus optional fields:
// Go
type Pizza struct {
Size string
Crust string
Toppings []string
}
func (p Pizza) String() string {
return fmt.Sprintf("Pizza{size=%s, crust=%s, toppings=[%s]}",
p.Size, p.Crust, strings.Join(p.Toppings, ", "))
}
type PizzaBuilder struct {
size, crust string
toppings []string
}
func NewPizzaBuilder() *PizzaBuilder {
return &PizzaBuilder{size: "Medium", crust: "Hand-tossed"}
}
func (b *PizzaBuilder) Size(s string) *PizzaBuilder { b.size = s; return b }
func (b *PizzaBuilder) Crust(c string) *PizzaBuilder { b.crust = c; return b }
func (b *PizzaBuilder) AddTopping(t string) *PizzaBuilder { b.toppings = append(b.toppings, t); return b }
func (b *PizzaBuilder) Build() Pizza {
cp := make([]string, len(b.toppings))
copy(cp, b.toppings) // defensive copy so the slice can't be mutated later
return Pizza{Size: b.size, Crust: b.crust, Toppings: cp}
}
func main() {
hawaiian := NewPizzaBuilder().
Size("Large").Crust("Thin").
AddTopping("Ham").AddTopping("Pineapple").
Build()
fmt.Println(hawaiian)
// Pizza{size=Large, crust=Thin, toppings=[Ham, Pineapple]}
plain := NewPizzaBuilder().Build()
fmt.Println(plain)
// Pizza{size=Medium, crust=Hand-tossed, toppings=[]}
}(Both programs were compiled and produce exactly the output shown in the trailing comments.)
The classic GoF form: a Director driving a Builder interface
The original Gang of Four formulation adds two pieces the fluent form drops: a Builder interface with one concrete builder per representation, and a Director that knows the construction recipe — the sequence of build steps — but not the concrete product. This separation pays off when one fixed assembly sequence must yield several different output formats.
The canonical example is a document generator: the same logical document — a title, a paragraph, a bullet list — rendered either as HTML or as Markdown. The Director encodes the recipe once; swapping the builder swaps the output format.
// Java (JDK 9+) — GoF Builder with a Director
// Builder interface: the construction steps, format-agnostic
interface DocBuilder {
void addTitle(String text);
void addParagraph(String text);
void addBullets(List<String> items);
String build(); // returns the finished representation
}
// Concrete builder #1 — HTML
class HtmlDocBuilder implements DocBuilder {
private final StringBuilder sb = new StringBuilder();
public void addTitle(String t) { sb.append("<h1>").append(t).append("</h1>\n"); }
public void addParagraph(String t) { sb.append("<p>").append(t).append("</p>\n"); }
public void addBullets(List<String> items) {
sb.append("<ul>\n");
for (String i : items) sb.append(" <li>").append(i).append("</li>\n");
sb.append("</ul>\n");
}
public String build() { return sb.toString(); }
}
// Concrete builder #2 — Markdown
class MarkdownBuilder implements DocBuilder {
private final StringBuilder sb = new StringBuilder();
public void addTitle(String t) { sb.append("# ").append(t).append("\n\n"); }
public void addParagraph(String t) { sb.append(t).append("\n\n"); }
public void addBullets(List<String> items) {
for (String i : items) sb.append("- ").append(i).append("\n");
sb.append("\n");
}
public String build() { return sb.toString(); }
}
// Director: owns the recipe, not the format
class ReportDirector {
private final DocBuilder builder;
ReportDirector(DocBuilder builder) { this.builder = builder; }
public String construct() {
builder.addTitle("Quarterly Report");
builder.addParagraph("Revenue grew this quarter.");
builder.addBullets(List.of("North up 12%", "EMEA flat", "APAC up 8%"));
return builder.build();
}
}
// Client: pick a builder, hand it to the director
String html = new ReportDirector(new HtmlDocBuilder()).construct();
String md = new ReportDirector(new MarkdownBuilder()).construct();
// html -> <h1>Quarterly Report</h1> ... <ul><li>North up 12%</li> ...
// md -> # Quarterly Report\n\nRevenue grew... \n- North up 12% ...Note what each role owns. The Director (ReportDirector) owns the order and choice of steps — "title, then paragraph, then bullets." The concrete builders own how each step renders. The client owns only the decision of which representation it wants. Add a PlainTextBuilder tomorrow and neither the Director nor the existing builders change.
Mapping this back to the four GoF roles: DocBuilder is the Builder, HtmlDocBuilder/MarkdownBuilder are ConcreteBuilders, ReportDirector is the Director, and the returned String is the Product. The classic Pizza-shop example phrases the same roles as a Waiter (Director) directing a HawaiianPizzaBuilder (ConcreteBuilder) to assemble a Pizza (Product).
When NOT to reach for a builder
A builder is not free. Every builder is an extra class (or nested class), an extra allocation per object you construct (the builder instance itself, separate from the product), and a layer of boilerplate that must be kept in sync with the product's fields. Use it only when that cost buys you something. Skip it when:
- The object has few fields. Two or three parameters do not need a builder — a plain constructor is shorter and clearer. A rough rule: under four parameters, prefer a constructor.
- All fields are required and have no sensible defaults. The builder's main payoff is gracefully handling optional fields. If every field must be supplied anyway, a constructor already forces that at compile time, while a builder can't easily guarantee a required field was set without runtime checks in
build(). - The object is immutable and small, and your language has a better tool. Java
records, Kotlin/Scala named-and-default arguments, Python keyword arguments and@dataclass, and C# object initializers all solve the "many named parameters" problem with less ceremony. Prefer the language feature; bring in a builder only when you also need staged construction, validation, or multiple representations. - You are tempted to add a Director but there is only one construction sequence and one product type. Then the Director is pure overhead — use the fluent builder and let the caller drive the steps.
Trade-offs at a glance
| Concern | Plain constructor | Fluent builder | GoF Director + Builder |
|---|---|---|---|
| Best when | Few, all-required fields | Many fields, several optional | One recipe, many output formats |
| Readability of call site | Poor with many args | High (named steps) | High; recipe centralized |
| Extra classes / allocation | None | One builder per build | Builder(s) + a Director |
| Enforces required fields | At compile time | Only via runtime checks | Only via runtime checks |
Key takeaways
- Reach for the fluent builder to tame objects with many optional parameters and to produce immutable results — this is the everyday use.
- Reach for the GoF Director form only when one fixed construction sequence must yield multiple representations (the HTML/Markdown document case).
- Always make a defensive copy of mutable inputs (collections) inside
build()so the finished product cannot be mutated through a retained builder reference. - Don't use a builder for small or all-required objects, and prefer your language's records / named-default arguments when they already solve the problem.
References
- Gamma, Helm, Johnson, Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (1994) — original Builder/Director formulation.
- Joshua Bloch, Effective Java, 3rd ed., Item 2: "Consider a builder when faced with many constructor parameters."
- Java Platform SE API:
java.util.List.of/List.copyOf(added in JDK 9). - Refactoring.Guru — Builder pattern (structure and intent reference).
Adapted and corrected from the original course page's Pizza-shop example, whose Waiter (Director) and HawaiianPizzaBuilder (ConcreteBuilder) map onto the GoF roles described above; the document-generator example is provided here to show the complete Director-driven variant end to end.
Interview drills & follow-ups
- "Builder vs Factory Method?" A factory does one-shot creation — one call returns a product, choosing which concrete type. A builder handles staged construction of one complex product across many optional steps, and can validate invariants before handing it back. Use a factory to pick a type; a builder to assemble one.
- "Are builders thread-safe?" No — a builder holds mutable partial state, so a single builder instance must not be shared across threads mid-build (a reused, un-cleared builder is the classic source of a flaky test where one product leaks another's fields). Give each thread its own builder; only the finished immutable product is safe to share.
- "How do you enforce a required field?" The fluent form can only check at runtime — validate in
build()and throw if a mandatory field was never set (e.g.if (size == null) throw new IllegalStateException("size required")). A plain constructor enforces the same at compile time, which is exactly why you skip the builder when every field is required.
🤖 Don't fully get this? Learn it with Claude
Stuck on Builder Pattern? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Builder Pattern** (OO & Low-Level Design) and want to truly understand it. Explain Builder Pattern from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Builder Pattern** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Builder Pattern** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Builder Pattern** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.