Fundamentals 20 min read

Builder Pattern in Java: Clean, Immutable Construction for Complex Objects

The article explains why traditional telescoping constructors and JavaBean setters fail for objects with many optional fields, and demonstrates how the Builder pattern—both the simplified static inner class version and the full GoF version with a Director—provides readable, step‑by‑step assembly, mandatory validation, and immutable results, with guidance on when to apply it.

Dabaoshi
Dabaoshi
Dabaoshi
Builder Pattern in Java: Clean, Immutable Construction for Complex Objects

The three factory patterns solve the "which one / which family" selection problem, but they do not address the challenge of assembling a single, highly complex object with many optional fields. The Builder pattern is introduced as the solution for clean, safe, step‑by‑step construction of such objects.

1. Telescoping Constructors: The Classic Constructor Explosion

When an Order class grows many fields—mandatory ones like order number and user ID, and numerous optional ones such as address, coupon, remark, invoice flag—the naive approach is to create a series of overloaded constructors to cover every combination. This leads to a rapid increase in constructor count and unreadable code.

public class Order {
    private final String orderNo; // mandatory
    private final long userId;    // mandatory
    private final String address; // optional
    private final String coupon;  // optional
    private final String remark; // optional
    private final boolean needInvoice; // optional
    public Order(String orderNo, long userId) { ... }
    public Order(String orderNo, long userId, String address) { ... }
    public Order(String orderNo, long userId, String address, String coupon) { ... }
    public Order(String orderNo, long userId, String address, String coupon, String remark) { ... }
    public Order(String orderNo, long userId, String address, String coupon, String remark, boolean needInvoice) { ... }
    // ... more overloads needed to skip specific fields
}

Constructor explosion: Adding a new optional field forces many new overloads, and some combinations cannot be expressed by overloads at all.

Poor readability: Calls like new Order("NO123", 1001L, null, null, "Fast delivery", true) make it impossible to tell which argument corresponds to which field.

Easy to misuse: With several parameters of the same type, swapping their order compiles but produces subtle runtime bugs.

The root cause is that constructors rely on positional parameters, which become a disaster when there are many optional arguments.

2. JavaBean + Setter: Readability at the Cost of Consistency

The alternative is to create an empty object and set fields one by one using setters, the classic JavaBean style:

Order order = new Order(); // create empty shell
order.setOrderNo("NO123");
order.setUserId(1001L);
order.setRemark("Fast delivery"); // optional fields can be set as needed
order.setNeedInvoice(true);

This improves readability because each setter name describes the field being set, but it introduces two serious problems:

Inconsistent intermediate state: Between new Order() and the final setter call, the object is a half‑finished product. If another thread accesses it or a required field is forgotten, the object is invalid.

Cannot be immutable: Because fields are assigned via setters, they cannot be final, and the class must remain mutable, losing the thread‑safety benefits of immutable objects.

Thus, telescoping constructors give one‑step construction and immutability but terrible readability, while JavaBean setters give readability but sacrifice consistency and immutability.

3. Builder Pattern: Chaining the Assembly Process

The Builder pattern separates the "assembly of parts" from the "final creation". A dedicated Builder object collects all fields (in any order) and, when build() is called, constructs a complete, immutable target object.

public class Order {
    private final String orderNo;
    private final long userId;
    private final String address;
    private final String coupon;
    private final String remark;
    private final boolean needInvoice;
    private Order(Builder b) {
        this.orderNo = b.orderNo;
        this.userId = b.userId;
        this.address = b.address;
        this.coupon = b.coupon;
        this.remark = b.remark;
        this.needInvoice = b.needInvoice;
    }
    public static class Builder {
        private String orderNo;
        private long userId;
        private String address;
        private String coupon;
        private String remark;
        private boolean needInvoice;
        public Builder orderNo(String v) { this.orderNo = v; return this; }
        public Builder userId(long v) { this.userId = v; return this; }
        public Builder address(String v) { this.address = v; return this; }
        public Builder coupon(String v) { this.coupon = v; return this; }
        public Builder remark(String v) { this.remark = v; return this; }
        public Builder needInvoice(boolean v) { this.needInvoice = v; return this; }
        public Order build() { return new Order(this); }
    }
}

Usage becomes a fluent, readable chain:

Order order = new Order.Builder()
    .orderNo("NO123")
    .userId(1001L)
    .remark("Fast delivery") // optional
    .needInvoice(true)        // optional
    .build();

Readability: Each field is set explicitly, no need to count positions.

Optional fields: Simply omit the setter for fields you don't need.

One‑step, complete creation: The target object is created only at build(), eliminating the "half‑finished" window.

Immutable result: All fields are final, the constructor is private, and no setters exist, so the object cannot be altered after creation.

The method‑chaining technique ( return this) implements a fluent interface, making the code read like a natural sentence.

4. Two Faces of the Builder: Simplified vs. GoF Original

The simplified version—static inner Builder with fluent setters— is what Effective Java popularizes and is used in most projects. The original GoF version adds a Director role that orchestrates the construction steps:

Builder: Defines the steps needed to build the product (e.g., buildPartA(), buildPartB()).

Director: Knows the order of those steps but not the details; it receives a Builder and invokes the steps in a fixed sequence.

When the construction sequence is important and reusable, the Director adds value; otherwise, the simplified Builder is sufficient.

5. Two Underrated Powers of the Builder

Power 1: Mandatory validation in build()

Because the object is fully assembled only at build(), you can perform a single comprehensive validation there:

public Order build() {
    if (orderNo == null || orderNo.isEmpty()) {
        throw new IllegalStateException("Order number is required");
    }
    if (userId <= 0) {
        throw new IllegalStateException("User ID is illegal");
    }
    if (needInvoice && address == null) {
        throw new IllegalStateException("Address required when invoice is needed");
    }
    return new Order(this);
}

This whole‑object check is impossible with JavaBean setters because there is no moment when all fields are guaranteed to be present.

Power 2: Producing a truly immutable object

All fields of Order are final, the constructor is private, and no setters exist. Once built, the object can be freely shared across threads without locking, guaranteeing thread‑safety.

6. When the Builder Is Worth the Cost

Builders add boilerplate code, so they are justified when:

The object has many parameters (typically more than four or five), especially many optional ones.

You need the resulting object to be immutable and validated at creation.

You want to avoid the constructor‑explosion and the JavaBean "half‑finished" window.

Builders are unnecessary when the object has only a few mandatory fields or when mutability is required.

7. Real‑World Appearances and the Boundary with Factories

Common Builder examples in Java include: StringBuilder / StringBuffer Lombok's @Builder annotation (generates the whole Builder automatically)

OkHttp's Request.Builder and

OkHttpClient.Builder
java.util.stream.Stream.builder()

and Calendar.Builder MyBatis's SqlSessionFactoryBuilder Factories focus on "which type to create" (selection), while Builders focus on "how to assemble a single complex instance". The two patterns can be combined: a factory may internally use a Builder to construct its product.

8. Summary

The Builder pattern addresses the "single complex object construction" problem that telescoping constructors and JavaBean setters cannot solve. It offers readable, step‑by‑step assembly, mandatory validation, and immutable results. Choose the simplified static Builder for most cases; add a Director only when the construction sequence is fixed and reusable. Remember the clear distinction: factories choose *what* to create, builders decide *how* to assemble it.

Builder pattern illustration
Builder pattern illustration
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design PatternsJavaValidationBuilder PatternImmutableObject Construction
Dabaoshi
Written by

Dabaoshi

Practical utilities

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.