🔨 Builder Design Pattern
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
Real-World Analogy
Think of ordering a custom burger at a restaurant. You don't get all ingredients dumped at once — instead, you go step-by-step: choose the bun, add patty, select cheese, add toppings, pick sauce. The builder (chef) constructs the burger incrementally, and you get the final product only when it's fully assembled. You could use the same step-by-step process to build a veggie burger or a double-patty burger.
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
A["🍞 Bun"] -->|"add"| B["🥩 Patty"] -->|"add"| C["🧀 Cheese"] -->|"add"| D["🥬 Toppings"] -->|"build!"| F["🍔 Burger!"]
style A fill:#FFF3E0,stroke:#E65100,color:#000
style B fill:#FCE4EC,stroke:#C62828,color:#000
style C fill:#FFF8E1,stroke:#F9A825,color:#000
style D fill:#E8F5E9,stroke:#2E7D32,color:#000
style F fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#000 🏗️ Structure
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
A["🟢 Director"] -->|"directs"| B[["🔨 Builder"]]
B -->|"extends"| C{{"🏠 House Builder"}}
B -->|"extends"| D{{"🏢 Office Builder"}}
C -->|"builds"| E(["🏠 House"])
D -->|"builds"| F(["🏢 Office"])
style A fill:#E8F5E9,stroke:#2E7D32,color:#000
style B fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#000
style C fill:#E3F2FD,stroke:#1565C0,color:#000
style D fill:#E3F2FD,stroke:#1565C0,color:#000
style E fill:#FFF8E1,stroke:#F9A825,color:#000
style F fill:#FFF8E1,stroke:#F9A825,color:#000 UML Class Diagram
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
class HouseDirector {
+constructLuxuryHouse(HouseBuilder) House
+constructSimpleHouse(HouseBuilder) House
}
class HouseBuilder {
<<interface>>
+buildFoundation() HouseBuilder
+buildStructure() HouseBuilder
+buildRoof() HouseBuilder
+addGarage() HouseBuilder
+addSwimmingPool() HouseBuilder
+addGarden() HouseBuilder
+getResult() House
}
class LuxuryHouseBuilder {
-house : House
+buildFoundation() HouseBuilder
+buildStructure() HouseBuilder
+buildRoof() HouseBuilder
+addGarage() HouseBuilder
+addSwimmingPool() HouseBuilder
+addGarden() HouseBuilder
+getResult() House
}
class House {
-foundation : String
-structure : String
-roof : String
-hasGarage : boolean
-hasSwimmingPool : boolean
-hasGarden : boolean
}
HouseDirector --> HouseBuilder : directs
LuxuryHouseBuilder ..|> HouseBuilder
LuxuryHouseBuilder ..> House : builds
style HouseDirector fill:#E3F2FD,stroke:#1565C0,color:#000
style HouseBuilder fill:#FFF3E0,stroke:#E65100,color:#000
style LuxuryHouseBuilder fill:#E8F5E9,stroke:#2E7D32,color:#000
style House fill:#F3E5F5,stroke:#6A1B9A,color:#000 ❓ The Problem
Without This Pattern
// Approach 1: Telescoping constructor — which parameter is which?!
HttpRequest request = new HttpRequest(
"https://api.example.com", // url
"POST", // method
"application/json", // contentType
"{\"key\":\"value\"}", // body
30000, // timeout
true, // followRedirects
null, // proxy
3, // retries
true, // keepAlive
null // certificate
);
// Approach 2: JavaBean setters — object is mutable and can be used half-configured
HttpRequest request2 = new HttpRequest();
request2.setUrl("https://api.example.com");
request2.setMethod("POST");
// Oops, forgot to set contentType... used in production incomplete!
request2.setBody("{\"key\":\"value\"}");
// Another thread reads request2 while we're still configuring it!
Problems:
- Telescoping constructors — 10+ parameters of similar types (
boolean,String,int) make it trivial to swap arguments silently; the compiler cannot catchnew HttpRequest(url, "POST", body, contentType, ...)vs the correct order - Parameter confusion — was
trueforfollowRedirectsorkeepAlive? Code review cannot tell without checking the constructor signature - Inconsistent state — with setters, the object can be passed around before all required fields are set, causing NullPointerExceptions deep in the call stack
- No immutability — setters force the object to remain mutable; any code with a reference can change it, creating thread-safety bugs
- Complex validation impossible — you cannot validate that "if method is GET, body must be null" because fields are set independently at different times
✅ The Solution
The Builder pattern solves this by:
- Extracting construction into a separate Builder class
- Providing fluent methods for each optional parameter
- Calling a
build()method that returns the final, validated, immutable object - The object is never in an inconsistent state — it's either under construction or fully built
🛠️ Implementation
The most common approach in modern Java — used by Lombok, Immutables, etc.
public final class HttpRequest {
// All fields are final — immutable after construction
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
private final int timeoutMs;
private final int maxRetries;
private final boolean followRedirects;
// Private constructor — only Builder can create instances
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
this.maxRetries = builder.maxRetries;
this.followRedirects = builder.followRedirects;
}
// Getters only — no setters
public String getUrl() { return url; }
public String getMethod() { return method; }
public Map<String, String> getHeaders() { return headers; }
public String getBody() { return body; }
public int getTimeoutMs() { return timeoutMs; }
public int getMaxRetries() { return maxRetries; }
public boolean isFollowRedirects() { return followRedirects; }
// Static factory to start building
public static Builder builder(String url) {
return new Builder(url);
}
// ========== The Builder ==========
public static class Builder {
// Required parameters
private final String url;
// Optional parameters with defaults
private String method = "GET";
private Map<String, String> headers = new HashMap<>();
private String body = null;
private int timeoutMs = 30_000;
private int maxRetries = 0;
private boolean followRedirects = true;
private Builder(String url) {
this.url = Objects.requireNonNull(url, "URL cannot be null");
}
public Builder method(String method) {
this.method = method;
return this;
}
public Builder header(String key, String value) {
this.headers.put(key, value);
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeoutMs(int timeoutMs) {
this.timeoutMs = timeoutMs;
return this;
}
public Builder maxRetries(int maxRetries) {
this.maxRetries = maxRetries;
return this;
}
public Builder followRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
return this;
}
// Validation happens at build time
public HttpRequest build() {
if (body != null && method.equals("GET")) {
throw new IllegalStateException("GET requests cannot have a body");
}
if (timeoutMs <= 0) {
throw new IllegalStateException("Timeout must be positive");
}
return new HttpRequest(this);
}
}
}
// ========== Usage ==========
HttpRequest request = HttpRequest.builder("https://api.example.com/users")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"name\": \"John\"}")
.timeoutMs(5000)
.maxRetries(3)
.build();
The GoF version — a Director orchestrates the build steps.
// Product
public class House {
private String foundation;
private String structure;
private String roof;
private boolean hasGarage;
private boolean hasSwimmingPool;
private boolean hasGarden;
// Setters used by builder (package-private)
void setFoundation(String f) { this.foundation = f; }
void setStructure(String s) { this.structure = s; }
void setRoof(String r) { this.roof = r; }
void setHasGarage(boolean g) { this.hasGarage = g; }
void setHasSwimmingPool(boolean p) { this.hasSwimmingPool = p; }
void setHasGarden(boolean g) { this.hasGarden = g; }
@Override
public String toString() {
return "House{foundation=" + foundation + ", structure=" + structure +
", roof=" + roof + ", garage=" + hasGarage +
", pool=" + hasSwimmingPool + ", garden=" + hasGarden + "}";
}
}
// Builder Interface
public interface HouseBuilder {
HouseBuilder buildFoundation();
HouseBuilder buildStructure();
HouseBuilder buildRoof();
HouseBuilder addGarage();
HouseBuilder addSwimmingPool();
HouseBuilder addGarden();
House getResult();
}
// Concrete Builder
public class LuxuryHouseBuilder implements HouseBuilder {
private House house = new House();
@Override
public HouseBuilder buildFoundation() {
house.setFoundation("Reinforced Concrete");
return this;
}
@Override
public HouseBuilder buildStructure() {
house.setStructure("Steel Frame with Glass");
return this;
}
@Override
public HouseBuilder buildRoof() {
house.setRoof("Solar Panel Roof");
return this;
}
@Override
public HouseBuilder addGarage() {
house.setHasGarage(true);
return this;
}
@Override
public HouseBuilder addSwimmingPool() {
house.setHasSwimmingPool(true);
return this;
}
@Override
public HouseBuilder addGarden() {
house.setHasGarden(true);
return this;
}
@Override
public House getResult() { return house; }
}
// Director — knows HOW to build specific configurations
public class HouseDirector {
public House constructLuxuryHouse(HouseBuilder builder) {
return builder.buildFoundation()
.buildStructure()
.buildRoof()
.addGarage()
.addSwimmingPool()
.addGarden()
.getResult();
}
public House constructSimpleHouse(HouseBuilder builder) {
return builder.buildFoundation()
.buildStructure()
.buildRoof()
.getResult();
}
}
In production, use Lombok to eliminate boilerplate.
import lombok.Builder;
import lombok.Getter;
import lombok.Singular;
@Getter
@Builder
public class HttpRequest {
private final String url;
@Builder.Default
private final String method = "GET";
@Singular // Generates addHeader() method
private final Map<String, String> headers;
private final String body;
@Builder.Default
private final int timeoutMs = 30_000;
@Builder.Default
private final int maxRetries = 0;
@Builder.Default
private final boolean followRedirects = true;
}
// Usage — identical fluent API, zero boilerplate
HttpRequest request = HttpRequest.builder()
.url("https://api.example.com")
.method("POST")
.header("Content-Type", "application/json")
.body("{\"key\": \"value\"}")
.timeoutMs(5000)
.build();
🎯 When to Use
- When an object has many optional parameters (more than 3-4)
- When you need to create immutable objects with complex initialization
- When the construction process must allow different representations of the product
- When you want to avoid telescoping constructors
- When object creation involves multiple steps that must be performed in a specific order
- When you need validation across multiple fields at construction time
🌍 Real-World Examples
| Framework / Library | Builder Usage |
|---|---|
StringBuilder / StringBuffer | Builds strings incrementally |
Stream.Builder | Stream.builder().add(1).add(2).build() |
Lombok @Builder | Auto-generates builder at compile time |
OkHttp Request.Builder | new Request.Builder().url(...).build() |
Spring WebClient | WebClient.builder().baseUrl(...).build() |
Protobuf | Message.newBuilder().setField(...).build() |
java.util.Locale.Builder | new Locale.Builder().setLanguage("en").build() |
HttpClient (Java 11+) | HttpClient.newBuilder().connectTimeout(...).build() |
Pitfalls
- Over-engineering simple objects — Don't use Builder for objects with 2-3 fields; a constructor is fine
- Mutable builder reuse — Reusing a builder instance can lead to shared state bugs; create a new builder for each object
- Missing required fields — Ensure required parameters are in the Builder constructor, not optional methods
- No compile-time safety for required fields — Builder can't enforce at compile time that all required methods were called (use Step Builder pattern for that)
- Duplicated field declarations — Fields exist in both the product and builder (Lombok solves this)
Key Takeaways
- Builder separates construction from representation — same process, different outputs
- Produces immutable objects without telescoping constructors
- Provides a fluent, readable API — code reads like English
- Validates at build time — object is never in an inconsistent state
- In modern Java: use Lombok
@Builderto eliminate boilerplate - In interviews: know both the GoF version (with Director) and the modern fluent version
- Builder is the go-to pattern when you see constructors with more than 3-4 parameters