Builder Pattern

9 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5

Builder Pattern

1. What Is It?

The pattern separates the construction of a complex object from its representation, allowing the same construction process to produce different results. Instead of a constructor that takes a dozen parameters (many of them optional), the client assembles the object step by step through a fluent , calling only the steps that are relevant.

Without , complex object construction degrades into the "telescoping constructor" anti-pattern — a class with ten overloaded constructors to handle every combination of optional parameters, or a single constructor with a twelve-parameter signature where the caller must remember that argument 7 is boolean useCompression and argument 8 is boolean useEncryption. Builder makes construction readable, enforces valid assembly order, and allows producing different representations from the same sequence of steps.


QUICK CHECK

A backend service needs to construct HTTP request objects with up to 12 optional configuration fields (timeouts, headers, retry policies, compression, authentication, etc.). The team currently uses a single constructor that accepts all 12 parameters, forcing callers to pass null or false for fields they don't need. Which problem does the Builder pattern most directly solve in this scenario?

Choose one answer

2. How It Works

Step-by-step mechanics:

  1. Define a listing every construction step (e.g., set_engine(), set_seats(), set_gps()).
  2. Implement one or more ConcreteBuilder classes — each builds a different kind of product.
  3. Each step returns self (Python) or this (Java) for method chaining.
  4. Add a build() method that assembles and returns the finished product.
  5. Optionally introduce a Director that defines named construction sequences (e.g., construct_sports_car()) — clients can skip the Director and call steps directly for custom configurations.

Python

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class Engine:
    volume: float
    horsepower: int

    def __str__(self) -> str:
        return f"Engine({self.volume}L, {self.horsepower}hp)"


@dataclass
class GPSNavigator:
    route: str = "default"


@dataclass
class Car:
    engine: Engine
    seats: int
    gps: Optional[GPSNavigator] = None
    trip_computer: bool = False
    convertible: bool = False

    def describe(self) -> str:
        parts = [f"Car: {self.engine}, {self.seats} seats"]
        if self.gps:
            parts.append(f"GPS({self.gps.route})")
        if self.trip_computer:
            parts.append("trip computer")
        if self.convertible:
            parts.append("convertible")
        return ", ".join(parts)


class CarBuilder:
    """Fluent builder — each setter returns self for method chaining."""

    def __init__(self) -> None:
        self._engine: Optional[Engine] = None
        self._seats: int = 2
        self._gps: Optional[GPSNavigator] = None
        self._trip_computer: bool = False
        self._convertible: bool = False

    def set_engine(self, engine: Engine) -> CarBuilder:
        self._engine = engine
        return self

    def set_seats(self, count: int) -> CarBuilder:
        self._seats = count
        return self

    def set_gps(self, gps: GPSNavigator) -> CarBuilder:
        self._gps = gps
        return self

    def set_trip_computer(self, enabled: bool) -> CarBuilder:
        self._trip_computer = enabled
        return self

    def set_convertible(self, enabled: bool) -> CarBuilder:
        self._convertible = enabled
        return self

    def build(self) -> Car:
        if self._engine is None:
            raise ValueError("Engine is required to build a Car")
        return Car(
            engine=self._engine,
            seats=self._seats,
            gps=self._gps,
            trip_computer=self._trip_computer,
            convertible=self._convertible,
        )


class Director:
    """Defines named construction sequences for common configurations."""

    def __init__(self, builder: CarBuilder) -> None:
        self._builder = builder

    def construct_sports_car(self) -> Car:
        return (
            self._builder
            .set_engine(Engine(volume=3.0, horsepower=450))
            .set_seats(2)
            .set_trip_computer(True)
            .set_convertible(True)
            .build()
        )

    def construct_city_car(self) -> Car:
        return (
            self._builder
            .set_engine(Engine(volume=1.2, horsepower=90))
            .set_seats(4)
            .set_gps(GPSNavigator(route="city"))
            .build()
        )


# Usage
builder = CarBuilder()
director = Director(builder)

sports_car = director.construct_sports_car()
print(sports_car.describe())
# Car: Engine(3.0L, 450hp), 2 seats, trip computer, convertible

custom_car = (
    CarBuilder()
    .set_engine(Engine(volume=2.0, horsepower=200))
    .set_seats(5)
    .set_gps(GPSNavigator(route="highway"))
    .build()
)
print(custom_car.describe())

Java

public class Engine {
    private final double volume;
    private final int horsepower;

    public Engine(double volume, int horsepower) {
        this.volume = volume;
        this.horsepower = horsepower;
    }

    @Override public String toString() {
        return String.format("Engine(%.1fL, %dhp)", volume, horsepower);
    }
}

public class GPSNavigator {
    private final String route;

    public GPSNavigator(String route) { this.route = route; }

    public String getRoute() { return route; }
}

public class Car {
    private final Engine engine;
    private final int seats;
    private final GPSNavigator gps;        // nullable
    private final boolean tripComputer;
    private final boolean convertible;

    private Car(Builder builder) {
        this.engine = builder.engine;
        this.seats = builder.seats;
        this.gps = builder.gps;
        this.tripComputer = builder.tripComputer;
        this.convertible = builder.convertible;
    }

    public String describe() {
        return String.format("Car: %s, %d seats%s%s%s",
            engine, seats,
            gps != null ? ", GPS" : "",
            tripComputer ? ", trip computer" : "",
            convertible ? ", convertible" : "");
    }

    // Static inner Builder — keeps Car immutable, avoids telescoping constructors
    public static class Builder {
        private Engine engine;                  // required
        private int seats = 2;                  // defaults
        private GPSNavigator gps = null;
        private boolean tripComputer = false;
        private boolean convertible = false;

        public Builder setEngine(Engine engine) {
            this.engine = engine;
            return this;
        }

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

        public Builder setGps(GPSNavigator gps) {
            this.gps = gps;
            return this;
        }

        public Builder setTripComputer(boolean enabled) {
            this.tripComputer = enabled;
            return this;
        }

        public Builder setConvertible(boolean enabled) {
            this.convertible = enabled;
            return this;
        }

        public Car build() {
            if (engine == null) throw new IllegalStateException("Engine is required");
            return new Car(this);
        }
    }
}

public class Director {
    public Car constructSportsCar() {
        return new Car.Builder()
            .setEngine(new Engine(3.0, 450))
            .setSeats(2)
            .setTripComputer(true)
            .setConvertible(true)
            .build();
    }

    public Car constructCityCar() {
        return new Car.Builder()
            .setEngine(new Engine(1.2, 90))
            .setSeats(4)
            .setGps(new GPSNavigator("city"))
            .build();
    }
}

// Usage
Director director = new Director();
Car sportsCar = director.constructSportsCar();
System.out.println(sportsCar.describe());
// Car: Engine(3.0L, 450hp), 2 seats, trip computer, convertible

Car customCar = new Car.Builder()
    .setEngine(new Engine(2.0, 200))
    .setSeats(5)
    .build();

QUICK CHECK

A developer is using the Builder pattern to construct API response objects and wants to call steps like .set_status(200).set_body(data).set_headers(headers).build() in a single expression. What must each setter method return to make this fluent chaining possible?

Choose one answer

3. Variants & Comparisons

ApproachHow It WorksProsConsBest For
Classic Builder (with Director)Director defines sequences; client calls directorNamed configurations; consistent buildsExtra Director classPreset configurations with multiple variants
Fluent Builder (no Director)Client chains steps directlyReadable; flexible ad-hoc configurationClient must know valid step combinationsAPIs and SDKs
Inner static Builder (Java)Builder is a static inner class of the productProduct stays immutable; builder has access to private fieldsJava-specific patternImmutable Java value objects
Telescoping constructor (anti-pattern)Overloaded constructors for every combinationNo extra classesUnreadable; error-proneAvoid entirely
Python kwargsCar(engine=..., seats=..., gps=...)No extra classes; idiomatic for simple casesNo validation or ordering; no reuseSimple objects with few optional params

QUICK CHECK

Your team is designing a public SDK for a cloud storage service. Developers using the SDK need to configure upload requests with various optional parameters (chunk size, retry policy, compression, encryption) in whatever order makes sense for their use case. Which builder variant best fits this scenario, and why?

Choose one answer

4. When to Use It (and When NOT To)

Use when:

  • An object requires more than 4–5 parameters, especially when many are optional.
  • Construction must proceed in a specific sequence with intermediate validation.
  • You want to reuse the same construction process to produce different representations.
  • You want to enforce that an object is never in an invalid half-constructed .

Do NOT use when:

  • The object is simple with few required fields — a constructor or @dataclass is enough.
  • You only ever build one configuration — the Director is unnecessary overhead.
  • You need to construct many small objects at high frequency — the extra object allocation per build adds up.

Anti-patterns:

  • Builder for everything — applying Builder to simple objects adds boilerplate without benefit.
  • Mutable product — the product should ideally be immutable after build(). A Builder that returns a mutable product that callers then mutate defeats the purpose.
  • Skipping validation in build()Builder is the right place to enforce invariants (e.g., "engine is required"). Skipping this means partially-built objects escape into the system.

Decision triggers:

  • "My constructor has more than 4 optional parameters" → Builder
  • "I want multiple named preset configurations" → Builder with a Director
  • "I need to construct the same object in different ways depending on context" → Builder

QUICK CHECK

A backend service needs to construct an HTTP request object that has 2 required fields (URL and method) and no optional fields. A developer proposes using the Builder pattern to construct these request objects, which are created thousands of times per second under high load. What is the most significant reason this is a poor fit for the Builder pattern?

Choose one answer

5. Real-World Usage

Java StringBuilder StringBuilder is the canonical standard-library . append() calls chain to assemble a string piece by piece, and toString() is the build() step that returns the final immutable String. The same construction process (repeated appends) can produce strings of any length and content.

SQLAlchemy Query (Python) SQLAlchemy's Query object chains .filter(), .join(), .order_by(), and .limit() calls before executing — a fluent Builder that assembles a SQL query object without executing it until .all() or .first() is called. This separation of construction from execution is the Builder pattern applied to query building.

Android AlertDialog.Builder / NotificationCompat.Builder Android's SDK uses Builder extensively for UI components. new AlertDialog.Builder(context).setTitle("…").setMessage("…").setPositiveButton("OK", listener).create() constructs a configured dialog object. The Builder ensures all required and optional fields are set before the dialog is created, and prevents a half-configured AlertDialog from being visible in the API.


QUICK CHECK

In SQLAlchemy, chaining .filter(), .join(), and .order_by() calls on a Query object does not immediately hit the database — the query only executes when .all() or .first() is called. Which aspect of the Builder pattern does this behavior directly demonstrate?

Choose one answer

6. Interview Cheat Sheet

Key sentences to demonstrate depth:

  1. " solves the telescoping constructor problem — when a class needs many optional parameters, a fluent is far more readable than overloaded constructors or a 12-argument constructor where you have to count positions."
  2. "The key invariant is that the product is never in a half-built, invalid visible to callers — validation happens in build(), not scattered across setters."
  3. "The Director is optional but valuable for codifying preset configurations — it separates the 'what steps to call' knowledge from the Builder itself, which stays reusable."
  4. "In Java, an inner static Builder class is the idiomatic form — it can access the product's private fields and produce an immutable result in one step."

Common follow-up questions:

  • "How is Builder different from ?" hides which concrete type is created; Builder controls how a single (potentially complex) object is assembled step by step. Factory answers "which class?"; Builder answers "how do we construct it?"
  • "Why is build() better than just returning the object from each setter?"build() is the single point where invariants are checked and the fully-configured immutable object is returned. Returning a partially-built object from intermediate setters would expose an invalid .
  • "When would you use Python dataclasses instead?" → For simple value objects with few optional fields. Once you need validation logic, ordering, or multiple named configurations, the Builder's explicit build() step is worth the extra code.

Connections to other concepts:

  • Fluent Builder commonly uses a fluent (method chaining), but the two are distinct: fluent interface is a style; Builder is a pattern with a specific construction purpose.
  • Factory MethodFactory Method answers "which type to create"; Builder answers "how to construct it". They are complementary and often used together.
  • Immutable objectsBuilder is the standard way to construct immutable objects that would otherwise require unwieldy constructors.
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.