Builder Design Pattern in Java: Build Complex Objects Step by Step
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
truefor the graphics card?Is
falsefor 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:
Create a Builder with the required information.
Configure optional properties using named methods.
Call
build()to validate the configuration.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.