Question 8 · Section 2

When to Use Builder?

Guarantees mandatory fields are filled at compile time:

Language versions: English Russian Ukrainian

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

  1. Objects with 1-2 parameters — constructor is more readable
  2. Value objects (Point(x,y)) — constructor or static factory method
  3. 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

  1. 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; }
    }
    
  2. 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

  1. Builder for >4 parameters or optional fields
  2. final fields — make the object immutable
  3. Validation in build() — last line of defense
  4. Step Builder for critical mandatory fields
  5. toBuilder() for immutable structures
  6. Lombok @Builder for code savings
  7. Avoid in Hot-path (GC pressure)
  8. @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