When to use Builder
The Builder pattern is a Creational design pattern designed to assemble complex objects step by step. It decouples the construction of an object from its internal representation...
🟢 Junior Level
The Builder pattern is a Creational design pattern designed to assemble complex objects step by step. It decouples the construction of an object from its internal representation, allowing the same construction process to produce different representations.
Core Problems Solved by Builder
1. The Telescoping Constructor Anti-Pattern
As domain entities grow, classes accumulate overloaded constructors with increasing numbers of arguments. Developers struggle to read and maintain them, and passing arguments of the same type (e.g., consecutive String or boolean parameters) easily leads to subtle inversion bugs:
// ❌ ANTI-PATTERN: What do these strings and booleans represent?
User user = new User("Alice", "Smith", "alice@example.com", "+18005550199", "Seattle", 28, true, false);
2. The Mutable JavaBeans Anti-Pattern (Setters)
Constructing a blank object and calling setters exposes the object in a partially populated, invalid state:
// ❌ HAZARDOUS: Object exists in a half-initialized, inconsistent state!
User user = new User();
user.setFirstName("Alice");
// If another concurrent thread reads 'user' right here, lastName and email are null!
user.setLastName("Smith");
The Solution: Classic Fluent Builder
public class User {
// Final fields guarantee immutability and safe publication
private final String firstName;
private final String lastName;
private final String email;
private final int age;
private User(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.email = builder.email;
this.age = builder.age;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String firstName;
private String lastName;
private String email;
private int age;
public Builder firstName(String firstName) {
this.firstName = firstName;
return this; // Fluent method chaining
}
public Builder lastName(String lastName) {
this.lastName = lastName;
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() {
// Validate cross-field invariants before creating the immutable object
if (firstName == null || lastName == null) {
throw new IllegalStateException("First name and last name are mandatory!");
}
return new User(this);
}
}
}
// Client usage:
User user = User.builder()
.firstName("Alice")
.lastName("Smith")
.email("alice@example.com")
.age(28)
.build();
When to Use Builder
- A class constructor accepts 4 or more parameters (especially of the same type).
- The class has numerous optional parameters with sensible default values.
- You want to construct strictly immutable (
final) objects with atomic invariant validation.
When NOT to Use Builder
- Simple classes with 1 to 3 mandatory parameters (a standard constructor is simpler and faster).
- Lightweight, short-lived DTOs or Value Objects.
- Ultra-low-latency hot paths (e.g., financial exchange matching engines) where generating millions of transient builder objects per second strains the Garbage Collector.
🟡 Middle Level
Safe Publication via final Fields (JLS §17.5)
An object assembled via a Builder with final fields benefits from Safe Publication guarantees under the Java Memory Model (JSR-133):
- The JVM guarantees that any thread obtaining a reference to the
Userobject afternew User(this)completes will observe the properly initialized values of allfinalfields without requiring explicit synchronization (volatileor locks). - A CPU hardware
StoreStorebarrier is emitted at the end of the constructor, ensuring all field writes are committed to cache before the object reference is made visible.
Cross-Field Invariant Validation
The build() method serves as an atomic transactional boundary where complex, interdependent business rules can be verified before instance publication:
public class BookingRequest {
private final LocalDate checkIn;
private final LocalDate checkOut;
private BookingRequest(Builder b) {
this.checkIn = b.checkIn;
this.checkOut = b.checkOut;
}
public static class Builder {
private LocalDate checkIn;
private LocalDate checkOut;
public Builder checkIn(LocalDate checkIn) { this.checkIn = checkIn; return this; }
public Builder checkOut(LocalDate checkOut) { this.checkOut = checkOut; return this; }
public BookingRequest build() {
if (checkIn == null || checkOut == null) {
throw new IllegalArgumentException("Check-in and check-out dates must not be null");
}
if (!checkOut.isAfter(checkIn)) {
throw new IllegalStateException("Check-out date must be strictly after check-in date!");
}
return new BookingRequest(this);
}
}
}
Lombok Builder Ecosystem: @Builder and @SuperBuilder
In enterprise development, Project Lombok eliminates manual builder boilerplate:
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder(toBuilder = true) // toBuilder enables non-destructive modification
public class AccountDto {
private final String accountId;
private final String ownerName;
private final BigDecimal balance;
}
// Updating an immutable instance (Wither semantics):
AccountDto updated = original.toBuilder()
.balance(BigDecimal.valueOf(1500.00))
.build();
Inheritance Support with @SuperBuilder
Standard @Builder does not support class hierarchies because the subclass builder cannot see or return parent builder fields. Lombok solves this with @SuperBuilder, utilizing recursive generic type bounds:
import lombok.experimental.SuperBuilder;
@SuperBuilder
public abstract class BaseEntity {
private final Long id;
private final Instant createdAt;
}
@SuperBuilder
public class Order extends BaseEntity {
private final String orderNumber;
}
// Client seamlessly configures both parent and child fields:
Order order = Order.builder()
.id(42L)
.createdAt(Instant.now())
.orderNumber("ORD-999")
.build();
🔴 Senior Level
Step Builder (Staged Builder) — Compile-Time Type Safety
A standard Builder discovers missing mandatory fields at runtime inside build(). A Step Builder forces callers to supply mandatory fields in a strict, compiler-enforced sequence using an interface pipeline:
public class EmailMessage {
private final String from;
private final String to;
private final String subject;
private final String body;
private EmailMessage(String from, String to, String subject, String body) {
this.from = from;
this.to = to;
this.subject = subject;
this.body = body;
}
// Step interfaces
public interface FromStep { ToStep from(String from); }
public interface ToStep { SubjectStep to(String to); }
public interface SubjectStep { OptionalSteps subject(String subject); }
public interface OptionalSteps {
OptionalSteps body(String body);
EmailMessage build();
}
public static FromStep builder() {
return new StepsExecutor();
}
private static class StepsExecutor implements FromStep, ToStep, SubjectStep, OptionalSteps {
private String from;
private String to;
private String subject;
private String body = "";
public ToStep from(String from) { this.from = from; return this; }
public SubjectStep to(String to) { this.to = to; return this; }
public OptionalSteps subject(String subject) { this.subject = subject; return this; }
public OptionalSteps body(String body) { this.body = body; return this; }
public EmailMessage build() { return new EmailMessage(from, to, subject, body); }
}
}
// Client usage: IDE autocomplete only offers the next valid mandatory step!
EmailMessage msg = EmailMessage.builder()
.from("sender@example.com") // Step 1: FromStep
.to("receiver@example.com") // Step 2: ToStep
.subject("Invoice Attached") // Step 3: SubjectStep
.body("Please review...") // Optional
.build();
// ❌ Will NOT compile: Calling build() before supplying mandatory steps is a compile error!
// EmailMessage.builder().from("a@b.com").build();
Escape Analysis and JIT Scalar Replacement
Every new Builder() call allocates an intermediate object in the JVM heap (Young Gen / Eden):
- Escape Analysis (C2 Compiler):
The JIT compiler analyzes the lifetime scope of the
Builderinstance. If the reference does not escape the allocating method (it is not passed to outside methods or stored into heap fields), the compiler applies Scalar Replacement: the builder’s fields are disassembled directly into CPU registers or stack slots. No actual heap allocation takes place. - When Escape Analysis Fails:
If builder method chains become overly deep, invoke non-inlinable virtual methods, or cross compilation thresholds, the JIT compiler cannot prove non-escape. In applications handling millions of operations per second, allocating millions of short-lived
Builderinstances causes substantial Garbage Collection Pressure and Eden space churn.
Java Records (Java 16+) vs Builder
Java Records provide immutable data modeling out of the box. However, Records generate only a single canonical constructor. When a record has 6 or more components or requires default values, pairing it with a static Builder remains the best practice:
public record UserRecord(String id, String username, String email, String role, boolean active) {
public UserRecord {
if (id == null || username == null) {
throw new IllegalArgumentException("ID and username are mandatory");
}
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String id;
private String username;
private String email;
private String role = "USER"; // Default value
private boolean active = true;
public Builder id(String id) { this.id = id; return this; }
public Builder username(String u) { this.username = u; return this; }
public Builder email(String e) { this.email = e; return this; }
public Builder role(String r) { this.role = r; return this; }
public Builder active(boolean a) { this.active = a; return this; }
public UserRecord build() {
return new UserRecord(id, username, email, role, active);
}
}
}
4 Tricky Questions
1. Why is the Builder pattern considered thread-safe compared to the JavaBeans pattern with setters?
Answer: The JavaBeans pattern constructs an empty object and populates it via setters over multiple statements. During this window, the object is exposed in a partially initialized, inconsistent state; concurrent threads reading the object can observe null or corrupted fields. In contrast, the Builder pattern populates fields in an isolated helper object and only creates the target domain object inside build(). Because the target object’s fields are declared final, the Java Memory Model (JSR-133 §17.5) guarantees Safe Publication: all threads are guaranteed to observe the fully initialized final fields without external locking.
2. How does the Step Builder (Staged Builder) pattern enforce mandatory parameters at compile time?
Answer: A Step Builder decomposes object assembly into a linked chain of focused interfaces. Each step interface exposes only a single method for a specific mandatory field, returning the next step’s interface. The terminal step interface exposes methods for optional fields and the final build() method. Because build() is unreachable until the caller traverses every mandatory step interface in the chain, missing a mandatory parameter results in a compile-time type mismatch rather than a runtime exception.
3. Under what conditions does the JIT compiler eliminate the heap allocation overhead of a Builder object?
Answer: The JIT compiler (specifically HotSpot C2) uses Escape Analysis to determine if the Builder object escapes the local method frame. If the builder is instantiated, invoked via inlined fluent methods, and discarded within the same method without being assigned to a field or passed to an un-inlined method, the JIT compiler applies Scalar Replacement. It breaks the builder into its primitive scalar components and allocates them directly in CPU registers or on the call stack, eliminating heap allocation and GC pressure entirely.
4. How does Lombok’s @SuperBuilder resolve the inheritance limitation of the standard @Builder annotation?
Answer: Standard @Builder generates a concrete builder whose methods return the builder’s concrete type, which fails in inheritance hierarchies because parent methods cannot return child builder instances. @SuperBuilder generates an abstract generic builder class with self-referential type bounds:
public abstract static class Init<C extends Child, B extends Init<C, B>> extends Parent.Init<C, B>.
This enables covariant return types across the entire inheritance tree, allowing fluent chaining of both parent and child fields while returning the correct child builder reference.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“The Builder pattern is a creational pattern used to construct complex objects with numerous parameters (typically 4 or more), especially when many are optional. It eliminates the telescoping constructor anti-pattern and prevents exposing partially initialized objects via mutable setters (JavaBeans). By populating
finalfields inside thebuild()method, Builder guarantees immutability and thread-safe publication under the Java Memory Model. For compile-time validation of mandatory fields, the Step Builder pattern can be used, while Lombok’s@Builderand@SuperBuilderremove boilerplate in enterprise applications.”
Key Architectural Trade-offs
- Builder vs Telescoping Constructors: Eliminates $2^N$ constructor permutations and improves readability.
- Builder vs JavaBeans Setters: Replaces mutable, half-initialized state with immutable, fully validated objects.
- Escape Analysis vs GC Pressure: JIT scalar replacement optimizes builders in small scopes, but high-throughput loops can still generate GC churn if escape analysis fails.
- Standard Builder vs Step Builder: Standard checks invariants at runtime; Step Builder enforces mandatory parameters at compile time.
Red Flags & Anti-Patterns
- 🚩 Using Builder on classes with 1–2 fields: Overengineering that clutters code with unnecessary classes.
- 🚩 Leaving target fields non-final: Defeats the primary thread-safety and immutability advantages of the Builder pattern.
- 🚩 Omitting invariant validation in
build(): Allowing the creation of semantically invalid objects. - 🚩 Adding public setters to a Builder-constructed class: Violates immutability principles.
Related Topics
- Difference between Factory Method and Abstract Factory — Alternative creational patterns
- What is Prototype pattern — Object creation via cloning
- What is Singleton — Single instance creational pattern
- What pattern categories exist — GoF creational taxonomy
- What anti-patterns do you know — Telescoping constructors and mutable state anti-patterns