The Builder Pattern is a creational design pattern that lets you construct complex objects step by step. This pattern is particularly useful when an object requires many configuration options, or when its constructor would otherwise have a "telescoping" list of parameters.
The Problem: Telescoping Constructors
Imagine you are building a Computer class for a website. A computer can have a CPU, RAM, GPU, Storage, and optional features like Bluetooth or a specialized cooling system.
Without a builder, your constructor might look like this:
public Computer(String cpu, String ram, String storage, boolean hasGpu, boolean isWaterCooled, boolean hasBluetooth) { ... }
If a user only wants a basic computer, they still have to pass false for every optional feature they don't need. It becomes hard to read and easy to mix up the order of the parameters.
Step-by-Step Java Implementation
1. The Product
The object we want to build. We make the constructor private so it can only be created via the Builder.
public class Computer {
// Required parameters
private final String cpu;
private final String ram;
// Optional parameters
private final String storage;
private final boolean hasGPU;
private final boolean hasBluetooth;
// Private constructor called by the Builder
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
this.hasGPU = builder.hasGPU;
this.hasBluetooth = builder.hasBluetooth;
}
@Override
public String toString() {
return "Computer [CPU=" + cpu + ", RAM=" + ram + ", Storage=" + storage +
", GPU=" + hasGPU + ", Bluetooth=" + hasBluetooth + "]";
}
// 2. The Static Inner Builder Class
public static class Builder {
private final String cpu;
private final String ram;
private String storage = "256GB SSD"; // Default value
private boolean hasGPU = false;
private boolean hasBluetooth = false;
public Builder(String cpu, String ram) {
this.cpu = cpu;
this.ram = ram;
}
public Builder setStorage(String storage) {
this.storage = storage;
return this; // Return 'this' to allow chaining
}
public Builder setGPU(boolean hasGPU) {
this.hasGPU = hasGPU;
return this;
}
public Builder setBluetooth(boolean hasBluetooth) {
this.hasBluetooth = hasBluetooth;
return this;
}
public Computer build() {
return new Computer(this);
}
}
}
Full Code for Testing
This implementation uses a "Fluent Interface" (method chaining) to make the object creation look clean and readable.
// Save as BuilderTest.java
class Computer {
private final String cpu;
private final String ram;
private final String storage;
private final boolean hasGPU;
private final boolean hasBluetooth;
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
this.hasGPU = builder.hasGPU;
this.hasBluetooth = builder.hasBluetooth;
}
@Override
public String toString() {
return "Configuration:\n" +
" - CPU: " + cpu + "\n" +
" - RAM: " + ram + "\n" +
" - Storage: " + storage + "\n" +
" - Dedicated GPU: " + (hasGPU ? "Yes" : "No") + "\n" +
" - Bluetooth: " + (hasBluetooth ? "Yes" : "No");
}
public static class Builder {
private final String cpu;
private final String ram;
private String storage = "256GB SSD";
private boolean hasGPU = false;
private boolean hasBluetooth = false;
public Builder(String cpu, String ram) {
this.cpu = cpu;
this.ram = ram;
}
public Builder setStorage(String storage) {
this.storage = storage;
return this;
}
public Builder enableGPU() {
this.hasGPU = true;
return this;
}
public Builder enableBluetooth() {
this.hasBluetooth = true;
return this;
}
public Computer build() {
return new Computer(this);
}
}
}
public class BuilderTest {
public static void main(String[] args) {
System.out.println("--- Building Custom PCs ---\n");
// Building a high-end Gaming PC
Computer gamingPC = new Computer.Builder("Intel i9", "32GB")
.setStorage("2TB NVMe")
.enableGPU()
.enableBluetooth()
.build();
// Building a simple Office PC (using defaults)
Computer officePC = new Computer.Builder("AMD Ryzen 5", "8GB")
.build();
System.out.println("Gaming PC " + gamingPC);
System.out.println("\nOffice PC " + officePC);
System.out.println("\n--- Build Complete ---");
}
}
Why use Builder?
- Immutability: You can make the
Computerfieldsfinal, ensuring the object cannot be changed once it is built. - Readability: Method names like
setStorageorenableGPUmake it clear what each value represents, unlike a long list of booleans in a constructor. - Control: It allows you to build objects in multiple steps (e.g., gathering data from different UI screens before calling
.build()).