Builder Pattern
The Builder pattern separates the construction of a complex object from its representation. Instead of a constructor with 10 parameters (half of which are optional), you get a readable, step-by-step construction process that produces an immutable result.
Why this matters: Any time you see an object with many optional fields โ orders, configurations, HTTP requests, database queries โ Builder is the right tool. It eliminates telescoping constructors and makes object creation self-documenting.
Prerequisites
- SOLID Principles โ Builder helps with SRP (separates construction from representation)
- Class Design Approach โ when to introduce Builder in your design
The Problem: Telescoping Constructors
flowchart LR
A["Order with 2 params"]:::client
B["Order with 5 params"]:::service
C["Order with 8 params"]:::data
D["Order with 12 params<br/>Which is the discount?<br/>Which is the coupon?"]:::client
A --> B
B --> C
C --> D
classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
When constructors grow, you get unreadable call sites:
// What does 'true' mean? What's 0.15? Is "SUMMER10" the coupon or the referral?
new Order("user-123", items, address, true, 0.15, null, "SUMMER10", null, Priority.HIGH, false, null, Instant.now());
The Solution: Fluent Builder
public class Order {
// All fields are final -- immutable after construction
private final String userId;
private final List<LineItem> items;
private final Address shippingAddress;
private final boolean giftWrap;
private final double discountRate;
private final String couponCode;
private final String referralCode;
private final Priority priority;
private final String notes;
private final Instant placedAt;
private Order(Builder builder) {
this.userId = builder.userId;
this.items = List.copyOf(builder.items); // defensive copy
this.shippingAddress = builder.shippingAddress;
this.giftWrap = builder.giftWrap;
this.discountRate = builder.discountRate;
this.couponCode = builder.couponCode;
this.referralCode = builder.referralCode;
this.priority = builder.priority;
this.notes = builder.notes;
this.placedAt = Instant.now();
}
// Getters only, no setters
public String getUserId() { return userId; }
public List<LineItem> getItems() { return items; }
public boolean isGiftWrap() { return giftWrap; }
// ... other getters
public static Builder builder(String userId) {
return new Builder(userId);
}
public static class Builder {
// Required
private final String userId;
private List<LineItem> items = new ArrayList<>();
private Address shippingAddress;
// Optional with defaults
private boolean giftWrap = false;
private double discountRate = 0.0;
private String couponCode = null;
private String referralCode = null;
private Priority priority = Priority.NORMAL;
private String notes = "";
private Builder(String userId) {
this.userId = Objects.requireNonNull(userId);
}
public Builder items(List<LineItem> items) {
this.items = new ArrayList<>(items);
return this;
}
public Builder addItem(LineItem item) {
this.items.add(item);
return this;
}
public Builder shippingAddress(Address address) {
this.shippingAddress = address;
return this;
}
public Builder giftWrap(boolean wrap) {
this.giftWrap = wrap;
return this;
}
public Builder discountRate(double rate) {
if (rate < 0 || rate > 1) throw new IllegalArgumentException("Rate must be 0-1");
this.discountRate = rate;
return this;
}
public Builder couponCode(String code) {
this.couponCode = code;
return this;
}
public Builder priority(Priority priority) {
this.priority = priority;
return this;
}
public Builder notes(String notes) {
this.notes = notes;
return this;
}
public Order build() {
// Validation before construction
if (items.isEmpty()) throw new IllegalStateException("Order must have items");
if (shippingAddress == null) throw new IllegalStateException("Shipping address required");
return new Order(this);
}
}
}
// Usage: self-documenting, compile-safe, immutable result
Order order = Order.builder("user-123")
.addItem(new LineItem("SKU-001", 2, Money.of(29.99)))
.addItem(new LineItem("SKU-042", 1, Money.of(149.00)))
.shippingAddress(warehouse.getNearestAddress(user))
.giftWrap(true)
.couponCode("SUMMER10")
.priority(Priority.HIGH)
.notes("Leave at door")
.build();
from dataclasses import dataclass, field
from typing import Optional
@dataclass(frozen=True) # Immutable
class Order:
user_id: str
items: tuple # immutable sequence
shipping_address: 'Address'
gift_wrap: bool = False
discount_rate: float = 0.0
coupon_code: Optional[str] = None
referral_code: Optional[str] = None
priority: str = "NORMAL"
notes: str = ""
class OrderBuilder:
def __init__(self, user_id: str):
self._user_id = user_id
self._items: list = []
self._shipping_address = None
self._gift_wrap = False
self._discount_rate = 0.0
self._coupon_code = None
self._referral_code = None
self._priority = "NORMAL"
self._notes = ""
def add_item(self, item) -> 'OrderBuilder':
self._items.append(item)
return self
def shipping_address(self, address) -> 'OrderBuilder':
self._shipping_address = address
return self
def gift_wrap(self, wrap: bool = True) -> 'OrderBuilder':
self._gift_wrap = wrap
return self
def discount_rate(self, rate: float) -> 'OrderBuilder':
if not 0 <= rate <= 1:
raise ValueError("Rate must be between 0 and 1")
self._discount_rate = rate
return self
def coupon_code(self, code: str) -> 'OrderBuilder':
self._coupon_code = code
return self
def priority(self, priority: str) -> 'OrderBuilder':
self._priority = priority
return self
def notes(self, notes: str) -> 'OrderBuilder':
self._notes = notes
return self
def build(self) -> Order:
if not self._items:
raise ValueError("Order must have at least one item")
if not self._shipping_address:
raise ValueError("Shipping address is required")
return Order(
user_id=self._user_id,
items=tuple(self._items),
shipping_address=self._shipping_address,
gift_wrap=self._gift_wrap,
discount_rate=self._discount_rate,
coupon_code=self._coupon_code,
referral_code=self._referral_code,
priority=self._priority,
notes=self._notes,
)
# Usage
order = (OrderBuilder("user-123")
.add_item(LineItem("SKU-001", 2, 29.99))
.add_item(LineItem("SKU-042", 1, 149.00))
.shipping_address(nearest_address)
.gift_wrap()
.coupon_code("SUMMER10")
.priority("HIGH")
.build())
class Order {
string userId_;
vector<LineItem> items_;
Address shippingAddress_;
bool giftWrap_;
double discountRate_;
optional<string> couponCode_;
Priority priority_;
string notes_;
// Private constructor -- only Builder can create
Order(const string& userId, vector<LineItem> items, Address addr,
bool gift, double discount, optional<string> coupon,
Priority prio, string notes)
: userId_(userId), items_(std::move(items)), shippingAddress_(std::move(addr)),
giftWrap_(gift), discountRate_(discount), couponCode_(std::move(coupon)),
priority_(prio), notes_(std::move(notes)) {}
friend class OrderBuilder;
public:
const string& getUserId() const { return userId_; }
const vector<LineItem>& getItems() const { return items_; }
bool isGiftWrap() const { return giftWrap_; }
};
class OrderBuilder {
string userId_;
vector<LineItem> items_;
Address shippingAddress_;
bool giftWrap_ = false;
double discountRate_ = 0.0;
optional<string> couponCode_;
Priority priority_ = Priority::NORMAL;
string notes_;
public:
explicit OrderBuilder(const string& userId) : userId_(userId) {}
OrderBuilder& addItem(LineItem item) {
items_.push_back(std::move(item));
return *this;
}
OrderBuilder& shippingAddress(Address addr) {
shippingAddress_ = std::move(addr);
return *this;
}
OrderBuilder& giftWrap(bool wrap) { giftWrap_ = wrap; return *this; }
OrderBuilder& discountRate(double rate) { discountRate_ = rate; return *this; }
OrderBuilder& couponCode(const string& code) { couponCode_ = code; return *this; }
OrderBuilder& priority(Priority p) { priority_ = p; return *this; }
OrderBuilder& notes(const string& n) { notes_ = n; return *this; }
Order build() {
if (items_.empty()) throw runtime_error("Order must have items");
return Order(userId_, std::move(items_), std::move(shippingAddress_),
giftWrap_, discountRate_, std::move(couponCode_),
priority_, std::move(notes_));
}
};
// Usage
auto order = OrderBuilder("user-123")
.addItem(LineItem("SKU-001", 2, 29.99))
.shippingAddress(nearestAddr)
.giftWrap(true)
.couponCode("SUMMER10")
.priority(Priority::HIGH)
.build();
When to Use vs When to Avoid
| Use Builder When | Avoid When |
|---|---|
| Object has 5+ parameters, many optional | 2-3 required fields, no optionals |
| Construction requires validation across fields | Validation is trivial |
| You want immutable objects | Mutability is fine (just use setters) |
| Same construction process should create different representations | Only one way to build the object |
| Readability at the call site matters | Internal code where a constructor is fine |
Builder vs Constructor vs Setters
| Approach | Pros | Cons |
|---|---|---|
| Constructor | Simple, enforces required params | Unreadable with 5+ params, no optionals |
| Setters | Flexible, clear names | Object is mutable, can be in invalid intermediate state |
| Builder | Readable, immutable result, validates on build | More boilerplate code |
Interview Questions
-
โWhen is Builder overkill?โ โ For objects with 2-3 fields that are all required, a constructor is cleaner. Builder adds value at 5+ fields or when optional fields exist.
-
โHow do you enforce required fields?โ โ Pass required fields to the Builder constructor. Optional fields get setter methods.
build()validates everything before constructing. -
โBuilder vs Factory?โ โ Factory chooses which class to instantiate. Builder constructs one class step-by-step. They solve different problems: Factory = โwhich object,โ Builder = โhow to configure it.โ
-
โHow does Lombokโs @Builder relate?โ โ Lombok generates the boilerplate. In interviews, write it manually to show you understand the pattern. Mention Lombok as a โin production Iโd use @Builderโ note.
-
โCan Builder be used with inheritance?โ โ Yes, but it gets tricky (generic self-referencing builders). In interviews, keep it flat. Real-world examples: Protobuf builders, OkHttp Request.Builder.