June 17, 2026 in patterns5 minutes
A intermediate-level guide to Builder: before-and-after java code and diagrams for a CS student.
Imagine you are building a User object. At first, you only need a first name and a last name. But as your application grows, you suddenly need an age, then an email address, then perhaps a phone number or a physical address.
In Java, one way to handle this is through constructor overloading—defining multiple versions of the same constructor with different parameter lists. However, when many of these parameters are of the same type (like String), you run into the “Telescoping Constructor” smell. You end up with a long chain of constructors where each one calls another, adding one more optional field at a time.
public class User {
private final String firstName;
private final String lastName;
private final int age;
private final String email;
// Telescoping constructors: hard to read, easy to swap parameters,
// and requires passing nulls for optional fields.
public User(String firstName, String lastName) {
this(firstName, lastName, 0, null);
}
public User(String firstName, String lastName, int age) {
this(firstName, lastName, age, null);
}
public User(String firstName, String lastName, int age, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.email = email;
}This approach is dangerous for two reasons. First, it forces the caller to pass null or default values (like 0) for every parameter they don’t care about, which makes the code hard to read. Second, because so many arguments are strings or integers, it is incredibly easy to accidentally swap the firstName with the lastName, a bug that the compiler will never catch.
classDiagram
User ..> User : calls constructor
note for User "Telescoping Constructors:\nUser(fName, lName)\nUser(fName, lName, age)\nUser(fName, lName, age, email)"
Think of ordering a custom pizza. You don’t shout every single detail at the chef in one massive, confusing sentence. Instead, you go through a process: first you choose the crust, then the sauce, then the toppings. If you don’t want extra cheese, you simply skip that step. You only finalize the order when you say “that’s all.”
The Builder pattern works exactly like this. Instead of passing a long list of arguments to a single constructor, you use a separate “Builder” object to collect your choices one by one. Only once you have specified everything you need do you call a final method to produce the actual object.
To fix the telescoping mess, we introduce a creational design pattern called the Builder. This pattern separates the construction of a complex object from its representation.
In this implementation, the User class has a private constructor that can only be called by the inner Builder class. The Builder provides “fluent” methods—methods that return this (the builder itself)—allowing you to chain calls together like a sentence.
public class User {
private final String firstName;
private final String lastName;
private final int age;
private final String email;
private User(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.email = builder.email;
}
public static class Builder {
private final String firstName;
private final String lastName;
private int age;
private String email;
public Builder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public User build() {
return new User(this);
}
}By using this approach, we achieve two major benefits:
.email()), so there is no ambiguity about what each argument represents.null for optional fields. If you don’t call .age() or .email(), those fields simply retain their default values within the builder.
sequenceDiagram
participant C as Client Code
participant B as Builder
participant U as User
C ->> B: new Builder(firstName, lastName)
B -->> C: builder instance
C ->> B: age(25)
B -->> C: builder instance
C ->> B: email("dev@example.com")
B -->> C: builder instance
C ->> B: build()
B ->> U: new User(builder)
U -->> C: user instance
One of the most powerful side effects of this pattern is that it enables immutability. In Java, an object is immutable if its state cannot be changed after it is created. We achieve this by declaring our fields as final and providing no “setter” methods.
Because the User can only be instantiated through the Builder, we ensure that once a User exists in your program, it is read-only and safe from unintended changes. The Builder acts as a temporary, mutable construction tool, while the User remains a permanent, stable record.
The Builder pattern is a powerful tool for managing complexity, but it is not a silver bullet.
Use it when:
Avoid it when:
Comparison of constructor usage vs Builder usage
// Problem: Hard to tell which String is which, and many nulls/defaults required
User user = new User("Jane", "Doe", 30, null);
// Solution: Fluent interface makes intent clear
User user = new User.Builder("Jane", "Doe")
.age(30)
.email("jane@example.com")
.build();