intermediate~2h

Builder Pattern

Learn how to construct a complex object with many optional parameters step by step and readably, avoiding both a dangerous positional constructor and combinatorial constructor explosion.

Learning objectives

  • Recognize when a constructor's parameter list has become long enough to be a real correctness risk
  • Build a chainable builder with named setters for optional fields and a build method
  • Explain why a builder is commonly implemented as a static nested class with access to a private constructor
  • Distinguish Builder's problem of safely constructing one complex object from Factory's problem of hiding which concrete class to instantiate

◆ Story

Ordering a custom sandwich at a counter works step by step: pick the bread, then the protein, then toppings, then sauce — each choice made clearly, in a readable sequence, with sensible defaults for anything you skip. Nobody hands the cashier one giant unlabeled list of twelve ingredients in a fixed order and hopes they got the sequence right.

That is exactly the difference between a long, positional constructor and the Builder pattern.

A House class needs to be constructed with six attributes: foundation, structure, and roof are mandatory — every house needs them — while garage, swimmingPool, and garden are optional, present on some houses and not on others. The obvious first version puts all six into one constructor. It is worth building that version first, and watching exactly why a constructor call full of same-typed parameters becomes a real risk, not just an inconvenience.

The obvious way to construct a House is a single constructor accepting all six fields — three strings for the mandatory attributes, three booleans for the optional ones.

This works. The house is built with exactly the six values passed in. But look closely at what a call site actually reads like: new House("concrete", "wood", "tile", true, false, true). What does "true, false, true" mean here, without going and checking the constructor's signature? Nothing about the call site tells you.

Now try building a second house that only cares about the three mandatory fields, letting the rest default sensibly. There is no clean way to do that. Java resolves constructor overloads by parameter types and count, not by name, so a constructor taking three strings cannot coexist with a different meaning for a different subset of three strings without genuinely different types or counts. The naive fix is constructor explosion — writing a separate constructor for every meaningful combination of optional fields present or absent — which can approach two-to-the-power-of-n constructors for n optional fields, wildly unmaintainable past a small handful of options.

Worse, the single big constructor is dangerous to call correctly even when you get the count right. Six positional parameters, three of them same-typed booleans in a row — swapping hasGarage and hasGarden compiles perfectly fine and silently builds the wrong house.

💻 Code example

class House { private final String foundation, structure, roof; private final boolean hasGarage, hasSwimmingPool, hasGarden; House(String foundation, String structure, String roof, boolean hasGarage, boolean hasSwimmingPool, boolean hasGarden) { this.foundation = foundation; this.structure = structure; this.roof = roof; this.hasGarage = hasGarage; this.hasSwimmingPool = hasSwimmingPool; this.hasGarden = hasGarden; } } // what does "true, false, true" even mean here, without checking the constructor's signature? House house = new House("concrete", "wood", "tile", true, false, true); // Now try building a second house that only cares about the three // mandatory fields, letting the rest default sensibly. There is no // clean way to do that with this constructor alone.

Mandatory fields should be required upfront — there is no meaningful default foundation, so those three belong together, provided once. Optional fields should each be set by name, not by position, so a method called setGarden(true) can never be confused with setGarage(true) the way two adjacent booleans can. Setting them should also be chainable and skippable, so a caller who does not care about the garden never has to think about it at all. Something needs to hold these in-progress values until everything is set, then assemble the real object — a second class, dedicated purely to construction, separate from House itself.

That is the Builder pattern: separate the construction of an object from its final representation, using a dedicated builder class with one clearly-named method per field, chained together, and ending in a build() call.

HouseBuilder lives as a static nested class inside House — static, because building a house does not require an existing House object to already exist first. Its own constructor takes only the three mandatory fields, required upfront. Each optional field gets its own setter, like setGarden(boolean hasGarden), which sets the field and then returns this — the same builder instance — which is exactly what enables chaining calls like .setGarden(true).setGarage(false) into one readable expression.

Finally, House gets a private constructor that reads its values from a finished builder, and HouseBuilder.build() calls it. Because HouseBuilder is nested inside House, it has access to House's private members, including that private constructor — a top-level class outside House would not. That is what actually makes the guarantee real: nothing outside House can call new House(...) directly at all, but HouseBuilder.build(), sitting inside the same outer class, can.

💻 Code example

class House { private final String foundation, structure, roof; private final boolean hasGarage, hasSwimmingPool, hasGarden; private House(HouseBuilder builder) { // PRIVATE -- a House can ONLY be built via HouseBuilder now this.foundation = builder.foundation; this.structure = builder.structure; this.roof = builder.roof; this.hasGarage = builder.hasGarage; this.hasSwimmingPool = builder.hasSwimmingPool; this.hasGarden = builder.hasGarden; } static class HouseBuilder { // static -- no House instance is needed to start building one private String foundation, structure, roof; private boolean hasGarage, hasSwimmingPool, hasGarden; // default to false automatically public HouseBuilder(String foundation, String structure, String roof) { // MANDATORY fields, upfront this.foundation = foundation; this.structure = structure; this.roof = roof; } public HouseBuilder setGarden(boolean hasGarden) { // named, not positional this.hasGarden = hasGarden; return this; // returns the SAME builder -- this is what enables chaining } public HouseBuilder setSwimmingPool(boolean hasSwimmingPool) { this.hasSwimmingPool = hasSwimmingPool; return this; } public HouseBuilder setGarage(boolean hasGarage) { this.hasGarage = hasGarage; return this; } public House build() { return new House(this); // hands the whole builder to House's private constructor } } }

Building a fully-specified house now reads as a clear, chained sequence: mandatory fields passed to the constructor upfront, then each optional feature set by name, ending in build() — nothing about "true, false, true" left to decode.

Building a second, simpler house proves the real payoff: skipping the garden and the swimming pool entirely just means calling fewer chained methods. hasSwimmingPool and hasGarden both come out false — Java's own default for an unset boolean field, applied automatically, with no second constructor and no need to remember argument positions. Compare this to the naive version, where skipping optional fields meant either a differently-shaped constructor call or carefully counting positions; here, the call site stays equally readable no matter how many fields are set or skipped.

💻 Code example

public class Main { public static void main(String[] args) { House house = new House.HouseBuilder("concrete", "wood", "tile") // mandatory fields, upfront .setGarden(true) .setSwimmingPool(true) .setGarage(false) .build(); // assembles the actual House -- nothing else can System.out.println(house); // A second house, skipping garden and swimming pool entirely: House simpleHouse = new House.HouseBuilder("concrete", "brick", "metal") .setGarage(true) // only sets what it cares about .build(); // hasSwimmingPool and hasGarden are both false -- Java's own // default for an unset boolean field, applied automatically. } }

▲ Edge case: validating before build() returns

Nothing shown so far stops build() from returning a House in an invalid state — say, an empty foundation string slipping through unnoticed. A more defensive build() checks its required fields and throws a clear IllegalStateException before constructing anything, rather than handing back a silently broken object that fails confusingly somewhere else later.

▲ Edge case: a builder for a genuinely small object is overkill

If House only had two fields, both mandatory, this entire pattern would be unnecessary ceremony — a plain constructor is perfectly readable at two or three parameters. Builder earns its cost specifically once a class has enough parameters, and enough of them optional, that a plain constructor becomes genuinely hard to call correctly — roughly four or more, as a practical rule of thumb, especially once several parameters share the same type.

▲ Common mistake: confusing Builder with Factory

Both are creational patterns, but they solve different problems. Factory Method is about hiding which concrete class to instantiate. Builder is about making the construction of one specific, complex object readable and safe, when it has many optional parameters. You can even combine them — a factory that internally uses a builder to actually assemble the object it eventually returns.

◆ Under the hood

Builder shows up anywhere an object has enough constructor parameters, especially optional ones, that a plain constructor call stops being safe to read at a glance.

  • Lombok's @Builder annotation — in a Spring Boot project, this generates exactly the nested static builder class shown in this chapter, automatically, without hand-writing the boilerplate yourself.
  • java.lang.StringBuilder — appends content step by step through chained calls, deferring the final, immutable String until toString() is called, the same "assemble first, finalize last" shape as build().
  • HTTP client request buildersjava.net.http.HttpRequest.newBuilder() and similar APIs across HTTP client libraries let you set headers, method, and body one chained call at a time before finalizing the request object.
  • Configuration objects — countless libraries expose a builder for their configuration classes specifically because most fields are optional, and a plain constructor with dozens of parameters would be unusable.

Q: What specific problem does Builder solve that a plain constructor cannot handle well? : Constructing an object with many parameters, especially optional ones, readably and safely — avoiding a long, error-prone positional parameter list and the constructor-explosion alternative.

Q: Why is HouseBuilder a static nested class rather than a top-level class? : Static, because no House instance needs to exist before you start building one; nested, specifically so it can access House's private constructor, which a top-level class could not.

Q: Why does every setter method on HouseBuilder return "this"? : To enable method chaining — each call returns the same builder instance, so another setter method can be called directly on the result.

Q: How does Builder differ from Factory? : Factory hides which concrete class to instantiate; Builder makes constructing one specific, complex object readable and safe when it has many optional parameters — they solve genuinely different problems.

Q: When is Builder overkill, and a plain constructor the better choice? : When a class has few parameters and most or all are mandatory — a plain constructor is perfectly readable at that scale, and the extra builder class would be unnecessary ceremony.

Want a visual for this concept?

Generate a diagram tailored to “Builder Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Prototype Pattern← Back to all Low-Level Design & Design Patterns chapters