Builder Pattern
Construct complex objects step by step. An intermediate creational pattern — the cure for telescoping constructors, unreadable new calls, and half-initialised objects.
Why Builder Exists
You are building an HTTP request library. A request can have a URL, a method, headers, query parameters, a body, a timeout, an authentication token, a retry policy, and a content type. Some fields are required, most are optional, and certain combinations are invalid (e.g. a GET with a body). You start with the obvious approach — a constructor. But every new optional field forces a new constructor overload. By the time you support all permutations, you have a wall of constructors with identical parameter types, and calling code that looks like new HttpRequest("https://api.example.com", "POST", null, null, body, 30, null, 3, "application/json"). Which null is the auth token? Which is the header map? Nobody knows without reading the constructor signature.
What goes wrong:
- Telescoping constructors — N optional fields produce up to 2N constructor overloads; the class becomes unmaintainable
- Unreadable call sites — positional arguments give no clue what each value means; bugs hide in the wrong parameter slot
- Invalid objects — nothing prevents constructing a GET request with a body, or a POST with no content type; constraints are unchecked
- Immutability sacrifice — the alternative (setters) requires making the object mutable, losing thread safety and allowing half-initialised states
This is the problem Builder solves — separate the construction of a complex object from its representation, so the same step-by-step process can create different configurations with named, readable, validated calls instead of positional constructor arguments.
What Is Builder?
Builder is a creational pattern that constructs a complex object step by step, letting you produce different types and representations using the same construction process. Instead of a monstrous constructor with 10 parameters, the client calls named methods — .url(), .method(), .header(), .timeout() — in any order they like, and finishes with .build() to get an immutable, fully validated object. The builder accumulates state incrementally; the product is assembled only at the end.
— Gamma, Helm, Johnson, Vlissides (1994)
.build() and get your finished, wrapped burrito (the Product). You can skip stations (no cheese today), go back (actually, add jalapeños), or visit the same station twice (extra salsa). The menu board listing the stations is the Director — it defines a standard order of operations, but you’re free to deviate.
Key insight: Builder trades a single large constructor for many small, named, chainable methods. Each method sets one concern and returns the builder itself (the fluent pattern), so calls chain naturally: builder.url(...).method(...).header(...).build(). The product stays immutable because all fields are set before construction completes. This separates it from Factory Method (which decides which class to instantiate) and from Abstract Factory (which creates families of products). Builder is about how to assemble a single complex object, not which object to create.
.url(), .header(), .timeout()this (the builder itself)builder.a().b().c().build() — reads like a sentencebuild() validates and assembles the final productParticipants & Structure
| Participant | Role | In the Analogy |
|---|---|---|
| Builder | An interface (or abstract class) declaring the step methods for constructing each part of the product. In the modern “fluent” style, this is often the inner static class itself. | The burrito counter — the contract guaranteeing stations for tortilla, rice, protein, toppings, and wrap. |
| Concrete Builder | Implements the builder interface. Stores intermediate state in private fields and returns this from each setter for fluent chaining. Provides a build() method that validates and assembles the product. | The chef behind the counter — accepts your choices, remembers them, and wraps the finished burrito. |
| Product | The complex object being constructed. Typically immutable — all fields are final and set via a private constructor that takes the builder. | The finished burrito — once wrapped, you can’t swap the rice for quinoa. |
| Director (optional) | Encapsulates a standard build sequence by calling builder steps in a fixed order. Useful when multiple clients need the same configuration. Often omitted in the fluent style. | The menu board — “Classic Burrito: flour tortilla, white rice, chicken, mild salsa, cheese.” A predefined recipe. |
| Client | Creates the builder, calls the desired step methods (or uses the director), and retrieves the product via build(). | You, the customer — choosing stations or asking for the Classic combo. |
- In Java, the Builder is almost always a
staticinner class of the Product. - This gives it access to the product’s private constructor while keeping them co-located.
- The GoF’s separate Builder interface and Director are useful when you need multiple representations (e.g. build an HTTP request as a
String, as abyte[], or as aHttpURLConnection), but for the common “fluent builder for one product” case, the inner class pattern is simpler and more widely used (seeStringBuilder,Stream.Builder, Lombok’s@Builder).
The Pattern In Motion
Scenario: Building an HTTP request for a JSON API call. The client uses the fluent builder to set a URL, method, headers, body, and timeout — then calls build() to get an immutable HttpRequest.
HttpRequest.builder()Builder instance is created with sensible defaults: method = "GET", headers = empty, timeout = 30s. No product exists yet..url("https://api.example.com/orders")this. The call is self-documenting — no positional guessing..method("POST").header("Content-Type", "application/json").body(json).timeout() keeps the default. The builder accumulates state incrementally..build()final — immutable from this point forward.HttpRequest- If your API has standard request patterns (e.g. “health check GET” or “authenticated POST with JSON body”), wrap them in a Director class with methods like
buildHealthCheck(url)andbuildJsonPost(url, body, token). - The Director calls builder steps in a fixed order, so common configurations don’t need to be spelled out everywhere.
- The client still has the builder for custom one-offs.
You Already Use This
Builder is one of the most visible patterns in the JDK. Any time you chain method calls and finish with a terminal operation that returns the final object, you’re using a builder — even if the JDK doesn’t always call it one.
java.lang.StringBuilder — the canonical JDK builder. You call .append() repeatedly (each returns this), then .toString() produces the immutable String. The builder accumulates characters; the product (String) is assembled at the end. java.util.stream.Stream.Builder — added in Java 8. Call Stream.builder().add(1).add(2).add(3).build() to get an immutable Stream<Integer>. The builder accepts elements one at a time; build() freezes the stream. java.net.http.HttpRequest.newBuilder() — added in Java 11. This is a perfect modern builder: HttpRequest.newBuilder().uri(uri).header(k,v).POST(bodyPublisher).build(). Fluent, validates at build(), returns an immutable HttpRequest. The JDK literally uses the Builder pattern for HTTP requests — the exact domain in our examples. StringBuilderis technically a builder but lacks thebuild()validation step — callingtoString()can never fail.- The Java 11
HttpRequest.newBuilder()is the purer example:build()validates (URI required, conflicting methods rejected) and the product is truly immutable. - When studying the pattern, focus on the Java 11 version.
Build It Once
Domain: HTTP Request Builder. The builder accumulates URL, method, headers, body, and timeout. build() validates constraints (URL required, POST needs body) and returns an immutable HttpRequest product.
- Need a retry policy? Add
.retries(int n)to the Builder — one new method, one new field. - Existing call sites that don’t use retries are unaffected because they never call the method and the default is sensible (0).
- Compare this to the constructor approach: you’d need a new overload and every existing
new HttpRequest(...)call would need review.
Common Mistakes
build() — defeating the entire purpose. The product must be immutable: all fields final, constructor private, no setters. If the product contains collections, use defensive copies (Map.copyOf(), List.copyOf()) in the constructor so the builder’s internal map can’t be used to mutate the product after construction.
build(): - The
build()method is your last line of defence. - If you don’t validate, the client can create an
HttpRequestwith no URL, a GET request with a body, or a negative timeout. - These bugs surface later at runtime — far from where the object was constructed.
- Always validate required fields, cross-field constraints, and range checks inside
build(). - Throw
IllegalStateExceptionwith a descriptive message.
build() without resetting: If the builder is reused, the second build() call may inherit leftover state from the first build. Either:
- Make each
build()call return a new builder (factory method returns fresh builder) - Document that the builder is single-use
- Reset internal state after
build()
- If your class has 2–3 required fields and no optional ones, a constructor is fine.
- Builder adds indirection, increases class count, and makes the code harder to navigate.
- Use Builder when there are 4+ fields, optional parameters, or cross-field validation.
- For simple value objects, a record or a constructor is clearer.
When To Use Builder
- The object has many fields (4+), especially when most are optional — the telescoping constructor problem
- You need the product to be immutable but can’t set all fields at once — the builder accumulates state, then freezes it
- There are cross-field constraints (e.g. POST requires body) that should be validated before the object exists
- You want readable call sites — named methods like
.timeout(10)are self-documenting; positional args are not - You need to build the same type of object in different configurations — the Director encapsulates common recipes
- The object has few fields (2–3) and all are required — a simple constructor or Java record is clearer
- The object is mutable by design (e.g. a DTO with setters) — there’s nothing to “freeze” at build time
- You need to create families of related objects — use Abstract Factory, not Builder
- You need to decide which class to instantiate at runtime — use Factory Method, not Builder
| Pattern | Creates | Focus | When to pick it |
|---|---|---|---|
| Builder ← this | One complex product | How to assemble โ step by step | Many optional fields, immutability, validation |
| Factory Method | One product type | Which class to instantiate | Subclass decides the product type at runtime |
| Abstract Factory | Family of related products | Which family to use | Products must be compatible (e.g. themed UI) |
| Prototype | Clone of existing object | Copy instead of construct | Creating from scratch is expensive; cloning is cheaper |
Problems To Solve
Builder problems test whether you can design a fluent API, enforce immutability, validate constraints at build time, and optionally use a Director for standard configurations.
| Difficulty | Problem | Key Insight |
|---|---|---|
| Easy | Pizza Order Builder Build a Pizza object with size (required), crust type, sauce, and a list of toppings (0–N). The pizza should be immutable after building. The client should be able to add toppings one at a time via .topping("mushrooms"). | The builder stores a List<String> for toppings. Each .topping() call appends to the list and returns this. build() validates that size is set and uses List.copyOf() for immutability. This tests the basic fluent pattern + collection handling. |
| Medium | SQL Query Builder Build a SqlQuery with table (required), selected columns (default: *), WHERE clauses (0–N), ORDER BY, and LIMIT. The builder should chain naturally: .from("users").where("age > 18").where("active = true").orderBy("name").limit(10).build(). | Multiple .where() calls accumulate clauses in a list, joined by AND in the final SQL string. build() validates that table is set. The tricky part: LIMIT without ORDER BY is usually a bug — should build() warn or throw? This tests builder design decisions and multi-value accumulation. |
| Medium | Email Builder with Director Build an Email object with to (required), cc, bcc, subject, body (text or HTML), and attachments. Create a Director with methods like buildWelcomeEmail(to) and buildPasswordReset(to, resetLink) that produce standard email templates. | The Director calls the builder in a fixed order with predefined content. The client can also use the builder directly for custom emails. build() validates that to is set and that body is present. This tests the GoF Director role alongside the fluent builder. |
| Hard | Type-Safe Step Builder Design an HTTP request builder where the compiler forces a specific order: URL must be set first, then method, then optional headers/body, then build(). Calling .build() before .url() should be a compile error, not a runtime exception. | Use a step builder (also called “wizard builder”): each step returns a different interface. builder() returns UrlStep; .url() returns MethodStep; .method() returns OptionalStep (which has .header(), .body(), and .build()). The type system prevents skipping required steps. This is advanced โ tests deep understanding of interfaces, generics, and API design. |
- When asked to implement Builder, the interviewer wants to see: (1) a Product with a private constructor and
finalfields; (2) a static inner Builder class with fluent setters that returnthis; (3) abuild()method that validates before constructing; (4) a demonstration that the product is immutable after building. - Bonus points: mention the Director for standard configurations, and contrast with the telescoping constructor anti-pattern.
- If asked about Lombok, note that
@Buildergenerates step 2 but skips step 3 (no validation) — you may still need a custombuild().