Builder

June 17, 2026 in patterns 5 minutes

A intermediate-level guide to Builder: before-and-after java code and diagrams for a CS student.

The Messy Constructor

Imagine you are building a User object. At first, you only need a first name and a last name. But as your application grows, you suddenly need an age, then an email address, then perhaps a phone number or a physical address.

In Java, one way to handle this is through constructor overloading—defining multiple versions of the same constructor with different parameter lists. However, when many of these parameters are of the same type (like String), you run into the “Telescoping Constructor” smell. You end up with a long chain of constructors where each one calls another, adding one more optional field at a time.

public class User {
    private final String firstName;
    private final String lastName;
    private final int age;
    private final String email;

    // Telescoping constructors: hard to read, easy to swap parameters,
    // and requires passing nulls for optional fields.
    public User(String firstName, String lastName) {
        this(firstName, lastName, 0, null);
    }

    public User(String firstName, String lastName, int age) {
        this(firstName, lastName, age, null);
    }

    public User(String firstName, String lastName, int age, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.email = email;
    }

This approach is dangerous for two reasons. First, it forces the caller to pass null or default values (like 0) for every parameter they don’t care about, which makes the code hard to read. Second, because so many arguments are strings or integers, it is incredibly easy to accidentally swap the firstName with the lastName, a bug that the compiler will never catch.

  classDiagram
    User ..> User : calls constructor
    note for User "Telescoping Constructors:\nUser(fName, lName)\nUser(fName, lName, age)\nUser(fName, lName, age, email)"

Intuition: Building Step by Step

Think of ordering a custom pizza. You don’t shout every single detail at the chef in one massive, confusing sentence. Instead, you go through a process: first you choose the crust, then the sauce, then the toppings. If you don’t want extra cheese, you simply skip that step. You only finalize the order when you say “that’s all.”

The Builder pattern works exactly like this. Instead of passing a long list of arguments to a single constructor, you use a separate “Builder” object to collect your choices one by one. Only once you have specified everything you need do you call a final method to produce the actual object.

The Solution: How the Builder Works

To fix the telescoping mess, we introduce a creational design pattern called the Builder. This pattern separates the construction of a complex object from its representation.

In this implementation, the User class has a private constructor that can only be called by the inner Builder class. The Builder provides “fluent” methods—methods that return this (the builder itself)—allowing you to chain calls together like a sentence.

public class User {
    private final String firstName;
    private final String lastName;
    private final int age;
    private final String email;

    private User(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.email = builder.email;
    }

    public static class Builder {
        private final String firstName;
        private final String lastName;
        private int age;
        private String email;

        public Builder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }

By using this approach, we achieve two major benefits:

  1. Clarity: Each value is assigned via a named method (like .email()), so there is no ambiguity about what each argument represents.
  2. Safety: We have eliminated the need to pass null for optional fields. If you don’t call .age() or .email(), those fields simply retain their default values within the builder.
  sequenceDiagram
    participant C as Client Code
    participant B as Builder
    participant U as User

    C ->> B: new Builder(firstName, lastName)
    B -->> C: builder instance
    C ->> B: age(25)
    B -->> C: builder instance
    C ->> B: email("dev@example.com")
    B -->> C: builder instance
    C ->> B: build()
    B ->> U: new User(builder)
    U -->> C: user instance

Immutability and Safety

One of the most powerful side effects of this pattern is that it enables immutability. In Java, an object is immutable if its state cannot be changed after it is created. We achieve this by declaring our fields as final and providing no “setter” methods.

Because the User can only be instantiated through the Builder, we ensure that once a User exists in your program, it is read-only and safe from unintended changes. The Builder acts as a temporary, mutable construction tool, while the User remains a permanent, stable record.

When to Use It, and When Not To

The Builder pattern is a powerful tool for managing complexity, but it is not a silver bullet.

Use it when:

  • You have a constructor with many parameters, especially several that are of the same type.
  • Many of those parameters are optional.
  • You want to create immutable objects.

Avoid it when:

  • Your class only has one or two required parameters and no optional ones; a standard constructor is simpler and more efficient.
  • The object is very simple; adding a Builder adds “boilerplate” code (extra lines of code that don’t add business logic) which can make the codebase harder to navigate for small, trivial objects.

Takeaways

  • Use a Builder when you face telescoping constructors with many optional parameters.
  • Builders prevent parameter-swapping bugs by using named methods instead of positional arguments.
  • The pattern enables immutability by allowing complex setup before the object is finalized.

Usage

Comparison of constructor usage vs Builder usage

// Problem: Hard to tell which String is which, and many nulls/defaults required
User user = new User("Jane", "Doe", 30, null);

// Solution: Fluent interface makes intent clear
User user = new User.Builder("Jane", "Doe")
        .age(30)
        .email("jane@example.com")
        .build();