Builder Pattern

Difficulty: Advanced

Question

What is the Builder pattern and what problem does it solve? Implement an immutable User using a Builder.

Answer

The Builder pattern separates the construction of a complex object from its representation, letting you build the object step by step through readable method calls. Let me show you the pain it removes before describing the pattern.

Suppose a User has two mandatory fields, name and email, and several optional ones: age, phone, address, and so on. With constructors you end up with the telescoping constructor anti-pattern: User(name, email), User(name, email, age), User(name, email, age, phone), and every combination. At the call site, new User("Asha", "asha@x.com", 21, null, "Pune", true, false) is unreadable, because you cannot tell which value is which, and swapping two values of the same type compiles without complaint. The other alternative, the JavaBeans style of a no-arg constructor plus setters, allows objects that are half-initialised and mutable, and it cannot be made immutable or thread-safe.

The Builder fixes both. You create a nested Builder class that takes the mandatory fields in its constructor, offers a fluent method for each optional field that returns this, and finishes with build(), which validates and returns the final object. The target class has a private constructor accepting the builder, so the only way to make a User is via the builder. Because all fields of User can be final, the resulting object is immutable, and validation happens once, in build(), so an invalid object can never exist.

The call becomes self-documenting: new User.Builder("Asha", "asha@x.com").age(21).phone("9999999999").build(). You can read exactly what is being set, optional fields can be skipped, and the order does not matter.

You have certainly used builders already: StringBuilder, Stream.Builder, Java's HttpRequest.newBuilder(), Locale.Builder, and Lombok's @Builder annotation that generates the boilerplate. Builders are also common in Protocol Buffers messages, test data creation, and query building libraries.

The original Gang of Four Builder also includes a Director class that knows the sequence of steps to build a particular configuration, and multiple builders that produce different representations from the same steps. In day-to-day Java, the fluent inner-builder is what people mean, and it is usually enough to mention the Director as a variation.

When to use it: when a constructor would have more than roughly four parameters, when many are optional, when you need immutability with validation, or when objects are built in steps. When not to use it: for simple classes with two or three fields, since it adds boilerplate. Java records with compact constructors, or static factory methods, might suffice.

Compare with Factory: a Factory decides which type to create and returns it in one call; a Builder constructs one complex type gradually. Also note that a builder is not thread-safe and should not be shared across threads, though the objects it produces are.

Code examples

Immutable User with a fluent Builder

class User {
    private final String name;
    private final String email;
    private final int age;
    private final String phone;

    private User(Builder b) {
        this.name = b.name;
        this.email = b.email;
        this.age = b.age;
        this.phone = b.phone;
    }

    static class Builder {
        private final String name;      // mandatory
        private final String email;     // mandatory
        private int age;                // optional
        private String phone;           // optional

        Builder(String name, String email) {
            this.name = name;
            this.email = email;
        }
        Builder age(int age)        { this.age = age; return this; }
        Builder phone(String phone) { this.phone = phone; return this; }

        User build() {
            if (age < 0) throw new IllegalStateException("age cannot be negative");
            return new User(this);
        }
    }

    @Override public String toString() {
        return "User{name=" + name + ", email=" + email + ", age=" + age + ", phone=" + phone + "}";
    }
}

public class BuilderDemo {
    public static void main(String[] args) {
        User u = new User.Builder("Asha", "asha@x.com")
                .age(21)
                .phone("9999999999")
                .build();
        System.out.println(u);
    }
}

Key points

Concepts covered

builder, design-patterns, creational, immutability, fluent-api