Creational

Builder Pattern

Construct complex objects step by step. An intermediate creational pattern — the cure for telescoping constructors, unreadable new calls, and half-initialised objects.

Overview ยท Creational ยท Singleton ยท Factory Method ยท Abstract Factory ยท Builder ยท Prototype ยท Adapter ยท Bridge ยท Composite ยท Decorator ยท Facade ยท Flyweight ยท Proxy ยท Observer ยท Strategy ยท Command ยท Template Method ยท Chain of Resp. ยท State ยท Mediator ยท Iterator ยท Visitor ยท Memento ยท Interpreter
Category: Creational Difficulty: Intermediate Interview: Tier 2 Confused with: Prototype
01
Section One ยท The Problem

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.

Naive approach — telescoping constructor
// โœ— Telescoping constructor โ€” grows with every optional field class HttpRequest { HttpRequest(String url) { ... } HttpRequest(String url, String method) { ... } HttpRequest(String url, String method, Map headers) { ... } HttpRequest(String url, String method, Map headers, String body, int timeout) { ... } // 7 more overloads for every combination... } // Caller: which null is which? new HttpRequest("https://api.com", "POST", null, null, body, 30, null, 3, "application/json");

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
Without Builder — telescoping constructor explosion
HttpRequest HttpRequest(url) HttpRequest(url, method) HttpRequest(url, method, headers) ... + 7 more new HttpRequest("url", "POST", null, null, body, 30, null, 3, "json") โ† which null is which? ✗ Unreadable, unscalable, error-prone — every optional field doubles the overloads

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.

02
Section Two ยท The Pattern

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.

GoF Intent: “Separate the construction of a complex object from its representation so that the same construction process can create different representations.”
— Gamma, Helm, Johnson, Vlissides (1994)
Analogy — ordering a custom burrito: At a burrito bar, you don’t hand the chef a 12-item order form in one shot. You walk along the counter and choose step by step: “flour tortilla” → “brown rice” → “black beans” → “chicken” → “salsa” → “no sour cream” → “guacamole.” Each station is a builder method. The chef (the Builder) accumulates your choices. When you reach the register, you call .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.

Builder exposes named methods like .url(), .header(), .timeout()
Call sites are self-documenting — every value is labelled, no positional ambiguity
Each setter returns this (the builder itself)
Fluent chaining: builder.a().b().c().build() — reads like a sentence
build() validates and assembles the final product
Invalid state (e.g. GET + body, missing URL) is caught at build time, not after the object is in use
Product class has only a private constructor taking the builder
Product is immutable after construction — no setters, no half-initialised state, thread-safe by default
03
Section Three ยท Anatomy

Participants & 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.
Builder — UML Class Diagram
HttpRequest - url : String - method : String - headers : Map - timeout : int all fields final, private constructor HttpRequest.Builder - url : String - method : String = "GET" - headers : Map = {} + url(String) : Builder + method(String) : Builder + header(k, v) : Builder returns this ← fluent + build() : HttpRequest creates » «optional» RequestDirector + buildGetRequest(url) + buildPostRequest(url, body) uses Client uses or uses director ■ Blue = Product (immutable) ■ Green = Builder (accumulates state) ■ Gold = build() method (validates & creates) ■ Dark = Director & Client ◆ solid = composition --โ–ท dashed = dependency
Inner class is the modern idiom:
  • In Java, the Builder is almost always a static inner 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 a byte[], or as a HttpURLConnection), but for the common “fluent builder for one product” case, the inner class pattern is simpler and more widely used (see StringBuilder, Stream.Builder, Lombok’s @Builder).
04
Section Four ยท How It Works

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.

Step 1 — Client creates a builder: HttpRequest.builder()
A new Builder instance is created with sensible defaults: method = "GET", headers = empty, timeout = 30s. No product exists yet.
Step 2 — Client chains: .url("https://api.example.com/orders")
The builder stores the URL in its internal field and returns this. The call is self-documenting — no positional guessing.
Step 3 — Client chains: .method("POST").header("Content-Type", "application/json").body(json)
Each method sets one concern and returns the builder. Order doesn’t matter. Skipping .timeout() keeps the default. The builder accumulates state incrementally.
Step 4 — Client calls .build()
The builder validates: URL must not be null; POST requires a body; timeout must be positive. If valid, it passes itself to the product’s private constructor. The product copies all fields as final — immutable from this point forward.
Step 5 — Client receives an immutable HttpRequest
The builder can be reused or discarded. The product has no setters — it’s thread-safe and cannot be placed in an invalid state after construction.
Before / After — telescoping constructor vs. Builder
BEFORE — telescoping constructor new HttpRequest( "url", "POST", null, null, body, 30, null, 3, "json" ) ✗ Which null is auth? Which is headers? ✗ No validation until runtime crash ✗ Mutable or overloaded AFTER — Fluent Builder HttpRequest.builder() .url("...").method("POST") .header("Content-Type", "json") .body(payload).build(); ✔ Every value labelled — self-documenting ✔ Validated at build() — immutable & thread-safe
Build Flow — step-by-step assembly
builder() .url() .method() .header() .body() .build() HttpRequest immutable ✔ Each step returns this → next step chains → build() validates → immutable product
The pattern in pseudocode
// โ”€โ”€ Fluent Builder (inner class style) โ”€โ”€ HttpRequest request = HttpRequest.builder() .url("https://api.example.com/orders") .method("POST") .header("Authorization", "Bearer tok_abc") .header("Content-Type", "application/json") .body("{\"item\":\"widget\",\"qty\":3}") .timeout(15) .build(); // validates, then returns immutable HttpRequest System.out.println(request); // POST https://api.example.com/orders // Headers: Authorization=Bearer tok_abc, Content-Type=application/json // Body: {"item":"widget","qty":3} // Timeout: 15s
Builder + Director:
  • 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) and buildJsonPost(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.
05
Section Five ยท Java Stdlib

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.

IN JAVA
Example 1 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.
Example 2 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.
Example 3 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.
Stdlib usage — StringBuilder
// Builder accumulates state โ†’ toString() produces immutable String String csv = new StringBuilder() .append("name").append(",") .append("age").append(",") .append("email") .toString(); // "name,age,email" โ€” immutable String
Stdlib usage — Java 11 HttpRequest.newBuilder()
// The JDK's own HTTP client uses Builder pattern HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/data")) .header("Accept", "application/json") .timeout(Duration.ofSeconds(10)) .GET() .build(); // immutable HttpRequest โ€” thread-safe, reusable HttpResponse<String> resp = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString());
StringBuilder vs. Builder pattern:
  • StringBuilder is technically a builder but lacks the build() validation step — calling toString() 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.
06
Section Six ยท Implementation

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.

Java — Builder Pattern HTTP Request (core)
// โ”€โ”€ Product: immutable, private constructor โ”€โ”€ final class HttpRequest { private final String url; private final String method; private final Map<String,String> headers; private final String body; private final int timeout; private HttpRequest(Builder b) { this.url = b.url; this.method = b.method; this.headers = Map.copyOf(b.headers); this.body = b.body; this.timeout = b.timeout; } public static Builder builder() { return new Builder(); } // โ”€โ”€ Builder: static inner class โ”€โ”€ public static class Builder { private String url; private String method = "GET"; private final Map<String,String> headers = new LinkedHashMap<>(); private String body; private int timeout = 30; public Builder url(String u) { this.url = u; return this; } public Builder method(String m) { this.method = m; return this; } public Builder header(String k, String v) { headers.put(k, v); return this; } public Builder body(String b) { this.body = b; return this; } public Builder timeout(int t) { this.timeout = t; return this; } public HttpRequest build() { if (url == null) throw new IllegalStateException("URL required"); if ("POST".equals(method) && body == null) throw new IllegalStateException("POST requires body"); return new HttpRequest(this); } } }
Extending the builder:
  • 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.
07
Section Seven ยท Watch Out

Common Mistakes

Mistake #1 — Making the product mutable after building: If the product has public setters, callers can mutate it after 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.
✗ Wrong — product has setters
// โœ— Product is mutable โ€” anyone can change it after build() class HttpRequest { private String url; public void setUrl(String u) { this.url = u; } // โ† breaks immutability }
✔ Correct — product is immutable
// โœ“ All fields final, private constructor, no setters final class HttpRequest { private final String url; private HttpRequest(Builder b) { this.url = b.url; } }
Mistake #2 — Skipping validation in build():
  • The build() method is your last line of defence.
  • If you don’t validate, the client can create an HttpRequest with 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 IllegalStateException with a descriptive message.
Mistake #3 — Reusing a builder after 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()
The safest approach: treat each builder instance as single-use. Create a new builder for each product.
Mistake #4 — Using Builder for simple objects:
  • 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.
08
Section Eight ยท Decision Guide

When To Use Builder

Use Builder When
  • 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
Avoid Builder When
  • 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
Builder vs. Confused Patterns
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
Decision Flowchart
Need to construct a complex object? No Use constructor Yes 4+ fields, many optional? No Constructor or record Yes Need immutability or cross-field validation? No JavaBean setters (mutable is OK) Yes Builder Step-by-step โ†’ validate โ†’ immutable product
09
Section Nine ยท Practice

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.
Interview Tip:
  • When asked to implement Builder, the interviewer wants to see: (1) a Product with a private constructor and final fields; (2) a static inner Builder class with fluent setters that return this; (3) a build() 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 @Builder generates step 2 but skips step 3 (no validation) — you may still need a custom build().