Skip to main content

Command Palette

Search for a command to run...

Builder Design Pattern in Java: Build Complex Objects Step by Step

Updated
10 min readView as Markdown

Imagine you are ordering a sandwich.

Bread and size are required, but the remaining ingredients are optional:

  • Cheese

  • Vegetables

  • Sauce

  • Extra paneer

  • Onion

You do not place your order like this:

Large, true, false, true, true, false

Nobody can easily understand what each true or false represents.

Instead, you say:

Give me a large sandwich, add cheese, add paneer, add mayonnaise, and do not add onion.

You are describing your sandwich step by step. After selecting everything, the final sandwich is prepared.

This is the basic idea behind the Builder Design Pattern.


What is the Builder Design Pattern?

The Builder Pattern is a creational design pattern used to construct a complex object step by step.

It is useful when:

  • A class has many constructor parameters.

  • Some parameters are required and others are optional.

  • The same object can have different configurations.

  • Object creation should be readable and controlled.

  • Values must be validated before creating the final object.

In simple words:

First configure the object using a Builder, then create the final object by calling build().


Understanding the Problem

Suppose we want to create a Computer object with the following properties:

  • Processor

  • RAM

  • Storage

  • Graphics card

  • Bluetooth

Without Builder Pattern, our class may look like this:

class Computer {

    private String processor;
    private int ram;
    private int storage;
    private boolean graphicsCard;
    private boolean bluetooth;

    public Computer(
            String processor,
            int ram,
            int storage,
            boolean graphicsCard,
            boolean bluetooth) {

        this.processor = processor;
        this.ram = ram;
        this.storage = storage;
        this.graphicsCard = graphicsCard;
        this.bluetooth = bluetooth;
    }
}

We create an object like this:

Computer computer =
        new Computer("Intel i7", 16, 512, true, false);

The code works, but it is confusing.

Look at these values:

true, false

Without checking the constructor, we cannot tell:

  • Is true for the graphics card?

  • Is false for Bluetooth?

  • What is the correct parameter order?

If the class contains 10 or 15 fields, object creation becomes even more difficult.


The Telescoping Constructor Problem

We might try to solve this problem using multiple constructors:

public Computer(String processor) {
}

public Computer(String processor, int ram) {
}

public Computer(String processor, int ram, int storage) {
}

public Computer(
        String processor,
        int ram,
        int storage,
        boolean graphicsCard) {
}

As the number of optional fields increases, we need more constructor combinations.

This is known as the Telescoping Constructor Problem.

The Builder Pattern solves this by replacing confusing constructor parameters with clearly named methods.


Builder Pattern Solution

class Computer {

    private final String processor;
    private final int ram;
    private final int storage;
    private final boolean graphicsCard;
    private final boolean bluetooth;

    private Computer(Builder builder) {
        this.processor = builder.processor;
        this.ram = builder.ram;
        this.storage = builder.storage;
        this.graphicsCard = builder.graphicsCard;
        this.bluetooth = builder.bluetooth;
    }

    public void showDetails() {
        System.out.println("Processor: " + processor);
        System.out.println("RAM: " + ram + " GB");
        System.out.println("Storage: " + storage + " GB");
        System.out.println("Graphics Card: " + graphicsCard);
        System.out.println("Bluetooth: " + bluetooth);
    }

    public static class Builder {

        private final String processor;
        private int ram = 8;
        private int storage = 256;
        private boolean graphicsCard = false;
        private boolean bluetooth = false;

        public Builder(String processor) {
            this.processor = processor;
        }

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

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

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

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

        public Computer build() {
            validate();
            return new Computer(this);
        }

        private void validate() {

            if (processor == null || processor.isBlank()) {
                throw new IllegalArgumentException(
                        "Processor is required");
            }

            if (ram <= 0) {
                throw new IllegalArgumentException(
                        "RAM must be greater than zero");
            }

            if (storage <= 0) {
                throw new IllegalArgumentException(
                        "Storage must be greater than zero");
            }
        }
    }
}

Main class

public class Main {

    public static void main(String[] args) {

        Computer gamingComputer =
                new Computer.Builder("Intel i9")
                        .ram(32)
                        .storage(1000)
                        .graphicsCard(true)
                        .bluetooth(true)
                        .build();

        gamingComputer.showDetails();

        System.out.println();

        Computer officeComputer =
                new Computer.Builder("Intel i5")
                        .ram(16)
                        .storage(512)
                        .build();

        officeComputer.showDetails();
    }
}

Output

Processor: Intel i9
RAM: 32 GB
Storage: 1000 GB
Graphics Card: true
Bluetooth: true

Processor: Intel i5
RAM: 16 GB
Storage: 512 GB
Graphics Card: false
Bluetooth: false

How Does the Builder Work?

Consider this code:

Computer computer =
        new Computer.Builder("Intel i9")
                .ram(32)
                .storage(1000)
                .graphicsCard(true)
                .bluetooth(true)
                .build();

Let us understand it step by step.

Step 1: Create the Builder

new Computer.Builder("Intel i9")

At this point, the final Computer object has not been created.

Only a temporary Builder object exists with these values:

processor = Intel i9
ram = 8
storage = 256
graphicsCard = false
bluetooth = false

The processor is required, while the remaining properties have default values.

Step 2: Configure the Builder

.ram(32)
.storage(1000)
.graphicsCard(true)
.bluetooth(true)

Every method updates one property inside the Builder.

For example:

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

This changes the Builder’s RAM value from 8 to 32.

Step 3: Build the Final Object

.build()

The build() method validates the configuration and creates the final object:

public Computer build() {
    validate();
    return new Computer(this);
}

The complete Builder object is passed to the private Computer constructor.

private Computer(Builder builder) {
    this.processor = builder.processor;
    this.ram = builder.ram;
    this.storage = builder.storage;
    this.graphicsCard = builder.graphicsCard;
    this.bluetooth = builder.bluetooth;
}

The constructor copies all values from the Builder to the final Computer object.

The complete flow is:

Create Builder
      ↓
Set optional properties
      ↓
Validate configuration
      ↓
Call build()
      ↓
Create final Computer object

Why Do Builder Methods Return this?

Consider this method:

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

Here, this represents the current Builder object.

Returning this allows us to call another method on the same Builder:

builder
    .ram(32)
    .storage(1000)
    .bluetooth(true);

This technique is known as method chaining or a fluent API.

Internally, the same Builder travels through the entire chain:

Builder
   → ram()
   → same Builder
   → storage()
   → same Builder
   → build()

Without return this, we would have to write:

Computer.Builder builder =
        new Computer.Builder("Intel i9");

builder.ram(32);
builder.storage(1000);
builder.bluetooth(true);

Computer computer = builder.build();

This also works, but method chaining is more readable.


Why Is the Computer Constructor Private?

private Computer(Builder builder)

The constructor is private so that outside code cannot directly create a Computer.

The caller must use the Builder:

Computer computer =
        new Computer.Builder("Intel i7")
                .ram(16)
                .build();

This gives us controlled object creation because:

  • Required fields cannot be skipped.

  • Optional fields get default values.

  • Validation happens before object creation.

  • The caller cannot bypass the creation rules.


Required and Optional Fields

In our example, the processor is required:

public Builder(String processor) {
    this.processor = processor;
}

Therefore, the caller must provide it:

new Computer.Builder("Intel i7")

The remaining fields are optional:

private int ram = 8;
private int storage = 256;
private boolean graphicsCard = false;
private boolean bluetooth = false;

If the caller does not provide them, their default values are used:

Computer basicComputer =
        new Computer.Builder("Intel i3").build();

This computer automatically receives:

RAM = 8 GB
Storage = 256 GB
Graphics Card = false
Bluetooth = false

Validation Before Object Creation

The Builder can validate the complete configuration before creating the final object.

public Computer build() {
    validate();
    return new Computer(this);
}

For example:

Computer computer =
        new Computer.Builder("Intel i7")
                .ram(-10)
                .build();

The Builder will reject this configuration:

RAM must be greater than zero

Therefore, an invalid Computer object is never created.


Builder and Immutability

The fields of Computer are declared as final:

private final String processor;
private final int ram;
private final int storage;

The Builder is mutable while we are configuring it, but the final Computer is immutable.

Mutable Builder → values can be configured

Immutable Computer → values cannot change after creation

This makes the final object safer because its state cannot change unexpectedly.


Advantages of the Builder Pattern

1. Readable object creation

Without Builder:

new Computer("Intel i9", 32, 1000, true, false);

With Builder:

new Computer.Builder("Intel i9")
        .ram(32)
        .storage(1000)
        .graphicsCard(true)
        .bluetooth(false)
        .build();

The Builder version clearly explains every value.

2. Easy handling of optional fields

The caller only provides the properties that are needed.

3. Parameter order does not matter

Both configurations produce the same result:

builder.ram(16).storage(512);
builder.storage(512).ram(16);

4. Centralized validation

All validation rules can be placed inside build().

5. Supports immutable objects

The final object can have private final fields without setters.

6. Different object configurations

The same Builder can create:

  • Basic computer

  • Office computer

  • Gaming computer


Disadvantages of the Builder Pattern

Builder Pattern also has some disadvantages:

  • It introduces an additional class.

  • It requires more code than a simple constructor.

  • Builder and Product may contain the same fields.

  • It is unnecessary for simple objects.

For example:

class Point {

    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Creating a Point is already simple:

Point point = new Point(10, 20);

Using Builder here would add unnecessary complexity.


When Should We Use Builder?

Use the Builder Pattern when:

  • The class contains many fields.

  • Several fields are optional.

  • Constructor calls are difficult to understand.

  • The object has different possible configurations.

  • The final object should be immutable.

  • Complete validation is required before object creation.

Common real-world examples include:

  • HTTP request creation

  • User profile creation

  • Order creation

  • Database query creation

  • Test data creation

  • UI component configuration


Builder Pattern vs Factory Pattern

Factory Pattern Builder Pattern
Decides which object to create Decides how to configure an object
Usually creates an object in one step Creates an object step by step
Useful for different implementations Useful for different configurations
Example: Email, SMS or Push notification Example: Basic, office or gaming computer

Factory example:

Notification notification =
        NotificationFactory.create("EMAIL");

Builder example:

Computer computer =
        new Computer.Builder("Intel i7")
                .ram(16)
                .storage(512)
                .build();

The easiest way to remember the difference is:

Factory focuses on what object to create. Builder focuses on how to configure and construct it.


Conclusion

The Builder Pattern is useful when an object contains many required and optional properties.

It creates the object in four simple steps:

  1. Create a Builder with the required information.

  2. Configure optional properties using named methods.

  3. Call build() to validate the configuration.

  4. Receive the final fully constructed object.

The most important idea is:

The Builder is temporary and configurable. The final object is created only when the complete configuration is ready.

That is why Builder Pattern makes object creation readable, flexible, validated and controlled.

More from this blog

Amit singh's blog

235 posts