How to implement Builder pattern for an immutable class?
Builder is justified for 4+ fields. For 1-2 fields use a constructor — Builder adds an extra class and complicates the code.
Junior Level
Builder is a pattern that helps create objects with many fields, especially when there are optional fields.
The problem: too many constructors
// "Telescoping constructors" — bad
new User("Ivan");
new User("Ivan", 25);
new User("Ivan", 25, "ivan@mail.com");
new User("Ivan", 25, "ivan@mail.com", "admin");
Solution: Builder
User user = new User.Builder("Ivan")
.age(25)
.email("ivan@mail.com")
.role("admin")
.build();
When Builder is excessive
Builder is justified for 4+ fields. For 1-2 fields use a constructor — Builder adds an extra class and complicates the code.
Middle Level
Classic implementation
public final class UserProfile {
private final String username; // required
private final String email; // optional
private final int age; // optional
private UserProfile(Builder builder) {
this.username = builder.username;
this.email = builder.email;
this.age = builder.age;
}
public String getUsername() { return username; }
public String getEmail() { return email; }
public int getAge() { return age; }
public static class Builder {
private final String username;
private String email;
private int age;
public Builder(String username) {
this.username = username; // required field
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public UserProfile build() {
return new UserProfile(this);
}
}
}
Lombok (@Builder)
@Value // final class, private final fields, equals/hashCode/toString
@Builder // @Builder generates a static Builder class with methods for each field.
public class UserProfile {
String username;
String email;
int age;
}
toBuilder() — for “modifying” an immutable object
UserProfile updated = user.toBuilder().age(30).build();
Senior Level
Validation in build()
The build() method is the ideal place for cross-field validation:
public UserProfile build() {
if (email != null && !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
return new UserProfile(this);
}
Lombok @Singular for collections
@Value @Builder
public class Order {
String id;
@Singular List<String> items; // automatic defensive copying
}
@Singular automatically calls List.copyOf() in build().
Why this matters for Senior
- Validation — object is not published until fully assembled and validated
- Atomicity — object either exists in a complete state or doesn’t exist at all
- API Design — builder is self-documenting, adding a field doesn’t break calls
- Immutability contract — builder is the only mutable place, the main class is fully immutable
Summary for Senior
- Builder for objects with 4+ fields
- Combine Builder with
finalfields of the main class - Lombok
@Builder+@Singular— minimal boilerplate with defensive copying - Always add
toBuilder()for convenient modification build()— place for final validation
Interview Cheat Sheet
Must know:
- Builder solves “telescoping constructors” — convenient parameter names via fluent API
- Classic implementation: static inner Builder class with chaining,
build()creates immutable object - Private constructor of immutable class accepts Builder — the only mutable place
- Lombok
@Builder+@Value— minimal boilerplate;@Singularautomatically callsList.copyOf() toBuilder()— for convenient “modification” of immutable objectbuild()— place for cross-field validation
Frequent follow-up questions:
- When is Builder justified? — 4+ fields, especially with optional ones
- What does @Singular do? — Automatic defensive copying of collections (
List.copyOf()) - Builder vs Record? — Record for simple DTOs; Builder for complex objects with validation
- Why private constructor? — So the object is only created through Builder with validation
Red flags (do NOT say):
- “Builder makes the class mutable” — Builder is mutable, the main class is final with final fields
- “Validation in Record constructor is better” — Builder allows validation BEFORE object creation
- “@Builder without @Singular is safe” — without @Singular collections are not copied
- “Builder is always needed” — for 1-2 fields a constructor is simpler
Related topics:
- [[3. How to create an immutable class in Java]]
- [[20. What is Record and how does it help create immutable classes]]
- [[25. Are there disadvantages to immutable objects]]
- [[29. How to properly work with collections in immutable classes]]