Design PatternsIntermediate.18 min read

Builder Pattern

Construct complex objects step by step with readable configuration, optional parameters and validation before the completed object is returned.

CreationalJavaObject ConstructionImmutability

Concept Overview

The Builder pattern separates the construction process from the final object. It is particularly useful when an object has several required values, many optional settings or validation rules. Instead of passing a long list of positional constructor arguments, the client describes the desired configuration through named builder methods and calls build() when the object is ready. The supplied Builder guide uses a Computer with CPU, RAM and an optional Bluetooth setting to demonstrate the idea.

1. The problem: telescoping constructors and unclear arguments

A constructor with many parameters becomes difficult to read, especially when several values share the same type or when booleans represent optional features. A call such as new Computer("Intel i5", "16GB", "512 SSD", true, false) does not explain the meaning of each boolean. Overloaded constructors can create a telescoping-constructor problem: every additional optional combination requires another overload or a confusing parameter set. The Builder pattern gives each setting a descriptive method name.

2. The core idea: configure, validate and build

The client starts with a builder, supplies required and optional settings in a readable sequence, and calls build() to create the final product. The builder holds temporary construction state. The product constructor can remain private so callers cannot bypass the builder's validation rules. In the supplied guide, CPU and RAM are required, while Bluetooth is optional.

Client construction flowjava
Computer officePc = Computer.builder()
    .cpu("Intel i5")
    .ram("16 GB")
    .bluetooth(true)
    .build();

3. Participants and responsibilities

The Product is the completed object. The Builder stores construction values and exposes fluent methods. Each configuration method updates one value and returns this so calls can be chained. The build() method validates the accumulated configuration and invokes the product's private constructor. In some Builder implementations a separate Director coordinates a fixed construction recipe; the guide's example does not require a Director because the client controls the sequence directly.

4. Java implementation: builder and immutable product

The Computer fields are final and are copied from the builder in a private constructor. The nested Builder stores mutable temporary values while the client configures the object. This lets the completed Computer remain immutable after construction. The example below follows the structure shown in the supplied guide while using ordinary Java syntax.

Computer.javajava
public final class Computer {
    private final String cpu;
    private final String ram;
    private final boolean bluetooth;

    private Computer(Builder builder) {
        this.cpu = builder.cpu;
        this.ram = builder.ram;
        this.bluetooth = builder.bluetooth;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private String cpu;
        private String ram;
        private boolean bluetooth;

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

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

        public Builder bluetooth(boolean bluetooth) {
            this.bluetooth = bluetooth;
            return this;
        }

        public Computer build() {
            if (cpu == null || cpu.isBlank() || ram == null || ram.isBlank()) {
                throw new IllegalStateException("CPU and RAM are required");
            }
            return new Computer(this);
        }
    }
}

Validation belongs at the construction boundary so an invalid Computer cannot be produced through the public builder API. Additional rules may validate supported CPU or RAM values.

5. Validate required and interdependent fields

The build() method is the natural place to verify that required values exist and that combined settings are valid. The supplied guide checks CPU and RAM before returning a Computer. A more complex report or configuration object may have required fields, mutually dependent optional values, ranges or format rules. For stronger compile-time guarantees, a staged builder can make required steps mandatory in the type system; runtime validation is still necessary for rules that depend on actual values.

6. Benefits and trade-offs

Builder calls are readable, optional settings are explicit, telescoping constructors are avoided and the final product can be immutable. The trade-off is additional code and another type to maintain. For a simple object with two obvious fields, a normal constructor or record may be clearer. Builder is valuable when construction has meaningful complexity; it should not be introduced automatically for every class.

  • Benefits: readable fluent calls, clear optional settings, immutable product support and centralised validation.
  • Trade-offs: more code, additional maintenance, and possible overengineering for simple objects.

7. Builder versus other creational patterns

Builder answers 'How do I assemble one complex object?' Factory Method answers 'Which concrete product should be created?' Abstract Factory answers 'Which family of compatible products is needed?' Singleton answers 'How is access to one shared instance controlled?' These patterns address different object-creation concerns and should not be substituted for one another merely because all involve construction or object access.

8. Practical selection and Java API note

Builder is a good fit for computers, reports, requests and configuration objects with many optional or interdependent settings. The supplied guide also notes that StringBuilder incrementally builds a mutable character sequence, but it is not itself the classic GoF Builder pattern example. The name alone does not make a class an instance of the design pattern.

9. Interview exercise: prevent invalid reports

The supplied guide ends with an exercise: design a Report with 12 optional fields and 3 required fields so invalid configurations cannot be built. A reasonable approach is to make required values part of the builder's mandatory construction path, provide descriptive methods for optional settings, and validate cross-field constraints in build(). If compile-time enforcement is important, use staged builder interfaces to prevent build() from being called until all required steps have been completed. Explain the trade-off: staged builders improve compile-time guarantees but add type and API complexity.

Key Takeaways

  • ✓Builder separates step-by-step configuration from the completed object.
  • ✓Use named methods instead of long positional constructor argument lists.
  • ✓Validate required and interdependent fields in build() before creating the product.
  • ✓Prefer Builder when construction complexity justifies its additional code.

References and further reading