Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
โฑ๏ธ 7 min read

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


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

  1. โ€œ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.

  2. โ€œHow do you enforce required fields?โ€ โ€“ Pass required fields to the Builder constructor. Optional fields get setter methods. build() validates everything before constructing.

  3. โ€œ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.โ€

  4. โ€œ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.

  5. โ€œ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.


See It in Action

E-commerce Cart

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access