When to Use Builder?
Guarantees mandatory fields are filled at compile time:
Junior Level
Builder is a pattern for step-by-step creation of complex objects.
Step-by-step = you call one method per field, in any order, and only build() creates the final object. The object never exists in a partially filled state.
Problem: When an object has many parameters, the constructor becomes unwieldy:
// "Telescoping" constructor — unclear what is what
User user = new User("Ivan", "Ivanov", "ivan@mail.com",
"+79001234567", "Moscow", 25, true, false);
Solution — Builder:
// Clear what is what
User user = User.builder()
.firstName("Ivan")
.lastName("Ivanov")
.email("ivan@mail.com")
.phone("+79001234567")
.city("Moscow")
.age(25)
.active(true)
.build();
When to use:
- More than 3-4 constructor parameters
- Optional parameters exist
- Need to make code readable
When NOT to use Builder
- Objects with 1-2 parameters — constructor is more readable
- Value objects (Point(x,y)) — constructor or static factory method
- Hot-path (millions of creations/sec) — overhead on builder object
Builder vs Factory Method
Builder — when many parameters (4+), especially optional ones. Factory Method — when there are several ways to create an object (fromJSON, fromXML, fromCSV). Constructor — when 1-2 mandatory parameters.
Middle Level
Problems Builder Solves
1. Telescoping constructors:
// 4 constructors for all combinations
public User(String name) { ... }
public User(String name, String email) { ... }
public User(String name, String email, int age) { ... }
public User(String name, String email, int age, String city) { ... }
// Builder — any combination
User.builder().name("Ivan").city("Moscow").build();
2. JavaBeans (setters) — unsafe:
// Object may be in an invalid state
User user = new User();
user.setName("Ivan");
user.setEmail("ivan@mail.com");
// ... intermediate state visible to other threads!
user.setAge(25);
user.validate(); // Forgot to call?
3. Builder guarantees the object is never in a partially filled state:
// All fields set before the object becomes accessible
User user = User.builder()
.name("Ivan")
.email("ivan@mail.com")
.age(25)
.build(); // Validation inside build()
// Object either fully created or exception thrown
Builder Implementation
public class User {
private final String name; // final — immutability!
private final String email;
private final int age;
private final String city; // Optional field
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.city = builder.city;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String name;
private String email;
private int age;
private String city;
public Builder name(String name) {
this.name = name;
return this; // Fluent API
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder city(String city) {
this.city = city;
return this;
}
public User build() {
// Validation
if (name == null || name.isEmpty()) {
throw new IllegalStateException("Name is required");
}
if (email == null || !email.contains("@")) {
throw new IllegalStateException("Valid email required");
}
return new User(this);
}
}
}
Lombok Builder
// Instead of 100 lines of code:
@Builder
public class User {
private final String name;
private final String email;
private final int age;
private final String city;
}
// Usage
User user = User.builder()
.name("Ivan")
.email("ivan@mail.com")
.build();
Typical Mistakes
- Builder for a simple object
// Overengineering @Builder public class Point { private int x; private int y; } // Constructor is sufficient public class Point { public Point(int x, int y) { this.x = x; this.y = y; } } - Mutable object after build
// Builder created, but fields are not final public class User { private String name; // Can be changed! } // final fields public class User { private final String name; // Cannot be changed }
Senior Level
Wither — method in functional programming that returns a new object with a changed field. GC pressure — frequent allocations trigger frequent GC cycles. Step Builder (Staged Builder) — compiler prevents skipping steps via interface types.
Step Builder (Staged Builder)
Guarantees mandatory fields are filled at compile time:
public class User {
private final String name; // Mandatory
private final String email; // Mandatory
private final String city; // Optional
private User(String name, String email, String city) {
this.name = name;
this.email = email;
this.city = city;
}
// Interfaces for each step
public interface NameStep { EmailStep name(String name); }
public interface EmailStep { CityStep email(String email); }
public interface CityStep {
CityStep city(String city);
User build();
}
// Implementation
public static NameStep builder() {
return new UserBuilder();
}
private static class UserBuilder implements NameStep, EmailStep, CityStep {
private String name;
private String email;
private String city;
public EmailStep name(String name) {
this.name = name;
return this;
}
public CityStep email(String email) {
this.email = email;
return this;
}
public CityStep city(String city) {
this.city = city;
return this;
}
public User build() {
return new User(name, email, city);
}
}
}
// Usage — compiler won't let you skip steps!
User user = User.builder()
.name("Ivan") // Mandatory
.email("i@m.com") // Mandatory
.city("Moscow") // Optional
.build();
// Won't compile:
User.builder().name("Ivan").build(); // Error: no email()!
Copy Builder (toBuilder)
@Builder(toBuilder = true)
public class User {
private final String name;
private final String email;
private final int age;
}
// Creating a copy with one field changed
User updated = user.toBuilder()
.email("new@email.com")
.build();
// Original user unchanged!
// -> Perfect for immutable structures
Validation in Builder
@Builder
public class Order {
private final LocalDate startDate;
private final LocalDate endDate;
private final BigDecimal amount;
@Builder
private Order(LocalDate startDate, LocalDate endDate, BigDecimal amount) {
// Cross-field validation
if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
throw new IllegalArgumentException("startDate must be before endDate");
}
if (amount != null && amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("amount cannot be negative");
}
this.startDate = startDate;
this.endDate = endDate;
this.amount = amount;
}
}
Performance: Memory Overhead
// Each Builder = additional object in Heap
User user = User.builder() // -> new Builder()
.name("Ivan")
.build();
// In Hot-path (1M objects/sec):
// -> 1M Builder objects -> GC pressure
// Benchmark:
// Ordinary constructor: 10ns
// Builder: 15ns (+50% overhead)
// For critical paths — use constructors!
Builder with Inheritance
// Problem: parent Builder returns Builder, not subclass
@Builder
public class User {
private String name;
}
@Builder
public class Admin extends User { // Lombok doesn't support this
private String role;
}
// Solution: @SuperBuilder
@SuperBuilder
public class User {
private String name;
}
@SuperBuilder
public class Admin extends User { // Works!
private String role;
}
Admin admin = Admin.builder()
.name("Ivan") // From parent
.role("ADMIN") // From subclass
.build();
Records + Builder
// Records don't have Builder by default
public record User(String name, String email, int age) {}
// Manual Builder for Records
public class User {
public static Builder builder() { return new Builder(); }
public static class Builder {
private String name;
private String email;
private int age;
public Builder name(String name) { this.name = name; return this; }
public Builder email(String email) { this.email = email; return this; }
public Builder age(int age) { this.age = age; return this; }
public User build() { return new User(name, email, age); }
}
}
// Or use Records with Lombok @Builder
@Builder
public record User(String name, String email, int age) {}
Production Experience
Real scenario #1: Builder prevented bugs
- DTO with 15 fields, 10 optional
- 2^10 = 1024 constructor combinations
- Solution: Builder -> any combination
- Result: -80% code, +readability
Real scenario #2: Step Builder prevented errors
- API client: 5 mandatory parameters
- Developers often forgot to fill them
- Solution: Step Builder
- Result: compile-time guarantee, 0 errors
Best Practices
- Builder for >4 parameters or optional fields
- final fields — make the object immutable
- Validation in build() — last line of defense
- Step Builder for critical mandatory fields
- toBuilder() for immutable structures
- Lombok @Builder for code savings
- Avoid in Hot-path (GC pressure)
- @SuperBuilder for inheritance
Senior Summary
- Builder = atomic creation + immutability
- Step Builder = compile-time guarantee of mandatory fields
- toBuilder = Wither logic for immutable objects
- Memory Overhead: +50% for creating Builder object
- Hot-path: avoid Builder — use constructors
- Validation: cross-field in build(), step-by-step in methods
- Records: manual Builder or Lombok @Builder
- Inheritance: @SuperBuilder solves the type problem
Interview Cheat Sheet
Must know:
- Builder solves telescoping constructor problem (4+ parameters) and unsafe JavaBeans (setters)
- Builder guarantees atomicity: object is never in a partially filled state
- Step Builder (Staged Builder) — compile-time guarantee of mandatory fields via interface types
- toBuilder() — Wither logic: creating a copy with changed fields for immutable objects
- Memory overhead: +50% for creating Builder object, avoid in Hot-path (1M+ creations/sec)
- Lombok @Builder saves ~100 lines of code, @SuperBuilder for inheritance
- Fields should be final — Builder without immutability loses its purpose
Common follow-up questions:
- When NOT to use Builder? — 1-2 parameters (constructor is more readable), value objects (Point), Hot-path (GC pressure)
- How does Builder differ from Factory Method? — Builder for many parameters (4+), Factory Method for different creation approaches
- What is Step Builder? — Pattern where the compiler prevents skipping mandatory steps via interface types
- Why should fields be final? — Immutability: after build() the object cannot be changed, otherwise Builder loses its point
Red flags (DO NOT say):
- “I use Builder for objects with 1-2 parameters” — overengineering, constructor is more readable
- “Builder isn’t needed with Lombok” — Lombok generates the same Builder, the principle remains
- “Fields can be mutable after build()” — then Builder doesn’t solve the safety problem
- “Step Builder is the same as ordinary Builder” — Step Builder gives compile-time guarantees
Related topics:
- [[07. Difference between Factory Method and Abstract Factory]] — creational patterns
- [[09. What is Prototype pattern]] — object copying
- [[03. What is Singleton]] — when Builder is needed for complex Singleton
- [[01. What are design patterns]] — general introduction
- [[02. What pattern categories exist]] — Creational patterns