Builder Design pattern

As applications grow, object creation often becomes complicated.
Consider a class with many data members:
public User(
String name,
String email,
String phone,
String address,
String company,
String department,
String country
) {}
Creating an object looks like:
User user = new User(
"Andrew",
"andrew@gmail.com",
null,
null,
null,
null,
"India"
);
At first glance, this might seem fine. But after a few months, it becomes difficult to remember:
What does the fifth parameter represent?
Which parameters are optional?
Did we accidentally swap two String arguments?
What happens when a new field is added?
This problem is commonly known as the Telescoping Constructor Problem, and it is one of the primary reasons the Builder Design Pattern exists.
The Builder Design Pattern solves this by constructing objects step-by-step instead of passing everything through a large constructor.
How Builder Works
The idea is simple:
Collect values inside a Builder object.
Configure fields using fluent methods.
Create the final object using
build().
User user = User.builder()
.name("Abhijit")
.email("abhijit@gmail.com")
.country("India")
.build();
The code becomes self-documenting because each value is associated with a field name.
Example: Pizza Builder
public enum Size { SMALL, MEDIUM, LARGE };
public enum Crust { THIN, THICK, STUFFED };
public class Pizza {
private final Size size;
private final Crust crust;
private final boolean cheese;
private List<String> toppings
private Pizza(Builder builder) {
this.size = builder.size;
this.crust = builder.crust;
this.cheese = builder.cheese;
this.toppings = builder.toppings;
}
public static class Builder {
private Size size;
private Crust crust = Crust.THIN;
private boolean cheese = false;
private List<String> toppings = new ArrayList<>();
public Builder size(Size size) {
this.size = size;
return this;
}
public Builder crust(Crust crust) {
this.crust = crust;
return this;
}
public Builder addTopping(String t) {
toppings.add(t);
return this;
}
public Builder cheese(boolean cheese) {
this.cheese = cheese;
return this;
}
public Pizza build() {
if (size == null) {
throw new IllegalStateException(
"Size is required"
);
}
return new Pizza(this);
}
}
}
Usage:
Pizza pizza = new Pizza.Builder()
.size(Size.MEDIUM)
.crust(Crust.THICK)
.topping("mozzarella")
.topping("mushrooms")
.topping("olives")
.cheese(true)
.build();
Instead of a large constructor, we configure only the fields we care about.
Real-World Example: HTTP Request Builder
Builders are heavily used in configuration-heavy APIs.
public enum Method {GET, PUT, DELETE, PATCH, POST};
class HTTPRequest {
private final Method method;
private final String body;
private final Map<String, String>headers;
private final int timeoutMs;
private final boolean followRedirects;
private final String url;
private HTTPRequest(Builder b) {
this.method = b.method;
this.url = b.url;
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(b.headers));
this.body = b.body;
this.timeoutMs = b.timeoutMs;
this.followRedirects = b.followRedirects;
}
public static Builder newBuilder(Method method, String url) {
return new Builder(method, url);
}
public static class Builder {
private final Method method;// required at entry
private final String url;// required at entry
private Map<String, String> headers = new LinkedHashMap<>();
private String body = null;
private int timeoutMs = 500;
private boolean followRedirects = true;
public HTTPRequest build() {
return new HTTPRequest(this);
}
public Builder header(String key, String value) {
headers.put(key, value); return this;
}
public Builder bearerToken(String token) {
return header("Authorization", "Bearer " + token);
}
public Builder addBody(String b) {
this.body = b;
return header("Content-Type", "application/json");
}
public addTimeouts(int time) {
timeoutMs = time;
return this;
}
public followRedirects(boolean f) {
followRedirects = f;
return this;
}
}
}
// GET request
HttpRequest getReq = HttpRequest.newBuilder(Method.GET, "https://api.example.com/users")
.bearerToken("my-token")
.header("Accept", "application/json")
.timeout(3000)
.build();
// POST request with JSON body
HttpRequest postReq = HttpRequest.newBuilder(Method.POST, "https://api.example.com/users")
.bearerToken("my-token")
.jsonBody("{\"name\":\"Andrew\",\"role\":\"admin\"}")
.timeout(10000)
.noRedirects()
.build();
Imagine passing all of these values through a constructor. The Builder version is much easier to understand and maintain.
This is why many libraries and SDKs use builders extensively.
Examples include:
StringBuilder
AWS SDK clients
HTTP clients
Database configuration objects
Validation and Immutability
A common benefit of Builder is centralized validation.
public User build() {
if (email == null) {
throw new IllegalStateException(
"Email is required"
);
}
return new User(this);
}
The Builder is usually mutable, but the final object is often immutable.
private final String email;
private final String name;
This prevents invalid or partially constructed objects from existing in the system.
Advantages
Eliminates large constructors
Improves readability
Supports optional parameters naturally
Centralizes validation in
build()Works well with immutable objects
Disadvantages
Overkill for small objects
Adds extra boilerplate code
Product and Builder often duplicate fields=
Introduces another class to maintain
When Should You Use Builder?
Use Builder when:
Objects contain many fields
Several parameters are optional
Validation is required before creation
Immutability is desired
Constructors become difficult to read
Avoid Builder for simple objects where a constructor is already clear and sufficient.
The Builder Design Pattern is primarily about improving object creation. Instead of relying on large constructors or numerous setters, it provides a structured and readable way to construct complex objects.
When an object's configuration starts becoming difficult to understand through constructors alone, Builder is often the cleanest solution.



