Builder Pattern
IntermediateSeparates the construction of a complex object from its representation, enabling the same construction process to create different representations via a fluent API.
Overview
The Builder pattern solves the telescoping constructor anti-pattern where a class with many optional fields requires an explosion of constructor overloads. Instead, a fluent Builder accumulates field values and produces an immutable object via a terminal build() call. Lombok's @Builder generates the builder at compile time. The pattern is ideal for value objects with many optional fields (HTTP requests, SQL queries, test fixtures). The Director class (optional) encapsulates step sequences to build standard configurations.
Telescoping Constructor Problem & Builder Solution
A class with 8 fields — where most are optional — forces callers to pass nulls or use many constructor overloads. Builder solves this with a readable, order-independent, immutable construction API.
// Anti-pattern: Telescoping constructor
public class HttpRequest {
public HttpRequest(String url) { ... }
public HttpRequest(String url, String method) { ... }
public HttpRequest(String url, String method, Map<String,String> headers) { ... }
// ... 5 more overloads — unreadable and error-prone
}
// Builder Pattern — immutable HttpRequest
public final class HttpRequest {
private final String url; // required
private final String method; // required
private final Map<String, String> headers;
private final String body;
private final int timeoutMs;
private final boolean followRedirects;
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = Collections.unmodifiableMap(builder.headers);
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
this.followRedirects = builder.followRedirects;
}
// Getters only — no setters, fully immutable
public String getUrl() { return url; }
public String getMethod() { return method; }
public static class Builder {
private final String url; // required — set in constructor
private final String method;
private Map<String, String> headers = new HashMap<>();
private String body;
private int timeoutMs = 30_000; // default
private boolean followRedirects = true;
public Builder(String url, String method) {
if (url == null || url.isBlank()) throw new IllegalArgumentException("url required");
if (method == null || method.isBlank()) throw new IllegalArgumentException("method required");
this.url = url;
this.method = method;
}
public Builder header(String key, String value) {
this.headers.put(key, value);
return this; // fluent — enables chaining
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeoutMs(int ms) {
if (ms <= 0) throw new IllegalArgumentException("timeout must be positive");
this.timeoutMs = ms;
return this;
}
public Builder followRedirects(boolean follow) {
this.followRedirects = follow;
return this;
}
public HttpRequest build() {
return new HttpRequest(this);
}
}
}
// Usage — readable, order-independent
HttpRequest request = new HttpRequest.Builder("https://api.aicancode.org/courses", "POST")
.header("Authorization", "Bearer token123")
.header("Content-Type", "application/json")
.body("{"title":"LLD Course"}")
.timeoutMs(5_000)
.followRedirects(false)
.build();Lombok @Builder
Lombok generates the builder boilerplate at compile time. @Builder on a class generates a static builder() method, a Builder inner class with setter methods, and a build() method. Use @Builder(toBuilder=true) to copy and modify existing instances.
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
@Builder(toBuilder = true) // toBuilder allows creating modified copies
public final class Course {
private final String id;
private final String title;
@Builder.Default private final String status = "DRAFT"; // default value
@Builder.Default private final int maxStudents = 100;
private final String instructorId;
private final List<String> tags;
}
// Usage
Course course = Course.builder()
.id("lld-101")
.title("Low Level Design")
.instructorId("akshay-123")
.tags(List.of("design-patterns", "java"))
.build();
// Copy with modification (toBuilder)
Course published = course.toBuilder()
.status("PUBLISHED")
.build();
System.out.println(published.getStatus()); // PUBLISHED
System.out.println(published.getTitle()); // Low Level Design (preserved)Key Points to Remember
- 1Builder eliminates telescoping constructors and prevents argument-order bugs (e.g. confusing two String params).
- 2Required fields belong in the Builder constructor; optional fields use fluent setters with defaults.
- 3The built object should be immutable — no setters, defensive copies on collections.
- 4Lombok @Builder generates builder boilerplate at compile time; @Builder(toBuilder=true) enables copy-and-modify.
- 5Director (optional) encapsulates standard build sequences — useful for predefined configurations.
- 6Validate invariants in build(), not in individual setter methods, for atomic validation.
Interview Questions
Sign in to ask AriaWhat problem does the Builder pattern solve that constructors cannot?
How do you enforce required fields in a Builder pattern?
What is the difference between Builder pattern and Factory Method pattern?
How does Lombok @Builder work and what does toBuilder=true do?
How would you make a Builder thread-safe?
Ask Aria about Builder Pattern
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.