Chapter 03 · Article 11 of 55

Builder Pattern - Deep Dive

Intent: Separate the construction of a complex object from its representation so that the same construction process can create different representations.

Article outline14 sections on this page

Overview

Intent: Separate the construction of a complex object from its representation so that the same construction process can create different representations.

The Builder pattern is a creational design pattern that addresses the problem of constructing complex objects step by step. Rather than requiring a massive constructor with numerous parameters or relying on setter methods that leave objects in inconsistent intermediate states, the Builder encapsulates the construction logic and exposes a clear, readable API for assembling the final product.

Classification: Creational Pattern
Scope: Object
Gang of Four Category: Creational
Complexity: **
Popularity:


Problem It Solves

The Telescoping Constructor Anti-Pattern

When a class has many fields - some required, some optional - developers often create multiple constructor overloads:

User(name)
User(name, email)
User(name, email, age)
User(name, email, age, phone)
User(name, email, age, phone, address, city, country, zipCode, ...)

This leads to:

  • Unreadable call sites - new User("John", null, 30, null, "123 St", null, "US", null) is cryptic
  • Parameter ordering errors - swapping two String arguments compiles fine but introduces bugs
  • Combinatorial explosion - with N optional params, you need up to 2^N constructors

Complex Object Creation with Many Optional Parameters

Objects like HTTP requests, database queries, or configuration objects have dozens of optional fields. Forcing callers to specify all of them (or pass nulls) violates usability and safety.

Immutable Object Construction

Immutable objects cannot use setters after construction. Without a builder, you must pass every field to the constructor at once - returning to the telescoping problem. The Builder pattern lets you accumulate state in a mutable builder, then produce an immutable product via build().


When to Use / When NOT to Use

When to UseWhen NOT to Use
Object has 4+ constructor parametersObject has ≤3 simple parameters
Many optional/configurable fieldsAll fields are required and obvious
Object must be immutable after creationMutability is acceptable (use setters)
Same construction process → different representationsOnly one representation ever needed
Construction involves multiple steps/validationsConstruction is trivial (single assignment)
You want a readable, self-documenting APIPerformance is ultra-critical (builder adds allocation)
Object has complex invariants to validate at build timeSimple POJO/data class suffices
You need to construct object trees (composite structures)Flat objects with no dependencies

Key Concepts & Theory

Director vs Builder

  • Builder - knows how to construct individual parts of the product. It exposes methods like buildWalls(), buildRoof(), etc.
  • Director - knows what sequence of steps to invoke on the builder. It orchestrates the construction order without knowing the details of each step.

The Director is optional. In the modern fluent-API style, the client often acts as its own director.

Fluent Interface & Method Chaining

Modern builders return this (or the builder instance) from each setter method, enabling chaining:

HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com")
    .method("POST")
    .header("Content-Type", "application/json")
    .body(payload)
    .timeout(30)
    .build();

Each method call reads like a sentence, making the code self-documenting.

Step-by-Step Construction

The builder accumulates state across multiple method calls. No intermediate object is exposed until build() is called. This guarantees the product is always in a valid, complete state when the client receives it.

Immutability

The product class typically has:

  • private final fields
  • No setters
  • A private constructor accepting only the Builder

The builder is the only way to create instances, enforcing immutability from the moment of creation.


ASCII Class Diagram

┌─────────────────────┐         ┌──────────────────────────────┐
│      Director       │         │     <<interface>>             │
│─────────────────────│         │         Builder               │
│                     │ uses    │──────────────────────────────│
│ - builder: Builder  │────────▶│ + buildStepA(): Builder      │
│                     │         │ + buildStepB(): Builder      │
│ + construct()       │         │ + buildStepC(): Builder      │
│                     │         │ + getResult(): Product       │
└─────────────────────┘         └──────────────┬───────────────┘
                                               │ implements
                                               │
                        ┌──────────────────────┴───────────────┐
                        │         ConcreteBuilder               │
                        │──────────────────────────────────────│
                        │ - partA: TypeA                        │
                        │ - partB: TypeB                        │
                        │ - partC: TypeC                        │
                        │──────────────────────────────────────│
                        │ + buildStepA(): Builder               │
                        │ + buildStepB(): Builder               │
                        │ + buildStepC(): Builder               │
                        │ + getResult(): Product                │
                        └──────────────────────┬───────────────┘
                                               │ creates
                                               ▼
                                ┌──────────────────────────┐
                                │         Product          │
                                │──────────────────────────│
                                │ - partA: TypeA           │
                                │ - partB: TypeB           │
                                │ - partC: TypeC           │
                                │──────────────────────────│
                                │ + getPartA(): TypeA      │
                                │ + getPartB(): TypeB      │
                                │ + getPartC(): TypeC      │
                                └──────────────────────────┘

Relationships:

  • Director has-a Builder (composition)
  • ConcreteBuilder implements Builder interface
  • ConcreteBuilder creates Product

Pseudocode Implementation

Example 1: Classic GoF Builder - Building a House

// Product
class House:
    foundation: String
    walls: String
    roof: String
    garage: Boolean
    swimmingPool: Boolean
    garden: Boolean

// Builder Interface
interface HouseBuilder:
    method buildFoundation(type: String): HouseBuilder
    method buildWalls(material: String): HouseBuilder
    method buildRoof(style: String): HouseBuilder
    method addGarage(): HouseBuilder
    method addSwimmingPool(): HouseBuilder
    method addGarden(): HouseBuilder
    method build(): House

// Concrete Builder
class ConcreteHouseBuilder implements HouseBuilder:
    private house: House = new House()

    method buildFoundation(type: String): HouseBuilder:
        house.foundation = type
        return this

    method buildWalls(material: String): HouseBuilder:
        house.walls = material
        return this

    method buildRoof(style: String): HouseBuilder:
        house.roof = style
        return this

    method addGarage(): HouseBuilder:
        house.garage = true
        return this

    method addSwimmingPool(): HouseBuilder:
        house.swimmingPool = true
        return this

    method addGarden(): HouseBuilder:
        house.garden = true
        return this

    method build(): House:
        validate()
        return house

    private method validate():
        if house.foundation == null:
            throw Error("Foundation is required")
        if house.walls == null:
            throw Error("Walls are required")

// Director  -  orchestrates construction sequences
class HouseDirector:
    private builder: HouseBuilder

    constructor(builder: HouseBuilder):
        this.builder = builder

    method constructLuxuryHouse(): House:
        return builder
            .buildFoundation("reinforced concrete")
            .buildWalls("brick and stone")
            .buildRoof("slate tiles")
            .addGarage()
            .addSwimmingPool()
            .addGarden()
            .build()

    method constructSimpleHouse(): House:
        return builder
            .buildFoundation("concrete slab")
            .buildWalls("wood frame")
            .buildRoof("asphalt shingles")
            .build()

// Client usage
director = new HouseDirector(new ConcreteHouseBuilder())
luxuryHouse = director.constructLuxuryHouse()
simpleHouse = director.constructSimpleHouse()

Example 2: Modern Fluent Builder - HTTP Request Object

// Immutable Product
class HttpRequest:
    private final url: String
    private final method: String          // GET, POST, PUT, DELETE
    private final headers: Map<String, String>
    private final body: String
    private final timeout: Integer
    private final retryCount: Integer
    private final followRedirects: Boolean
    private final authentication: AuthConfig

    // Private constructor  -  only Builder can instantiate
    private constructor(builder: Builder):
        this.url = builder.url
        this.method = builder.method
        this.headers = immutableCopy(builder.headers)
        this.body = builder.body
        this.timeout = builder.timeout
        this.retryCount = builder.retryCount
        this.followRedirects = builder.followRedirects
        this.authentication = builder.authentication

    // Static factory to get builder
    static method builder(url: String): Builder:
        return new Builder(url)

    // Inner static Builder class
    static class Builder:
        // Required
        url: String
        // Optional with defaults
        method: String = "GET"
        headers: Map<String, String> = new HashMap()
        body: String = null
        timeout: Integer = 30
        retryCount: Integer = 0
        followRedirects: Boolean = true
        authentication: AuthConfig = null

        constructor(url: String):
            if url == null or url.isEmpty():
                throw Error("URL is required")
            this.url = url

        method method(m: String): Builder:
            this.method = m
            return this

        method header(key: String, value: String): Builder:
            this.headers.put(key, value)
            return this

        method body(body: String): Builder:
            this.body = body
            return this

        method timeout(seconds: Integer): Builder:
            if seconds <= 0:
                throw Error("Timeout must be positive")
            this.timeout = seconds
            return this

        method retryCount(count: Integer): Builder:
            this.retryCount = count
            return this

        method followRedirects(follow: Boolean): Builder:
            this.followRedirects = follow
            return this

        method authentication(auth: AuthConfig): Builder:
            this.authentication = auth
            return this

        method build(): HttpRequest:
            // Final validation
            if this.method == "POST" and this.body == null:
                throw Error("POST requests require a body")
            return new HttpRequest(this)

// Client usage  -  reads like documentation
request = HttpRequest.builder("https://api.example.com/users")
    .method("POST")
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer token123")
    .body('{"name": "Alice"}')
    .timeout(10)
    .retryCount(3)
    .followRedirects(false)
    .build()

Builder vs Constructor vs Factory

CriteriaConstructorFactory MethodBuilder
Number of paramsFew (1-3)Few to moderateMany (4+), especially optional
Object complexitySimpleModerateHigh
Readability at call siteGood for few paramsGood (named method)Excellent (self-documenting)
Immutability supportYes, but unwieldy with many paramsYesYes - best approach
Variants/familiesNoYes (subclass decides)Yes (different builders)
Step-by-step constructionNoNoYes
Validation timingAt constructionAt factory callAt build() - after all fields set
Use whenObject is simpleYou need polymorphic creationObject is complex with optional config
OverheadNoneMinimalExtra Builder class

Rule of thumb: If you find yourself adding a 4th parameter to a constructor, consider a Builder. If you need to choose which class to instantiate at runtime, use a Factory. If both apply, combine them - a Factory that returns Builders.


Real-World Examples

1. StringBuilder / StringBuffer (Java, C#)

Accumulates characters/strings via append(), produces final String via toString(). Avoids creating intermediate immutable String objects.

2. java.net.http.HttpRequest.Builder (Java 11+)

HttpRequest.newBuilder()
    .uri(URI.create("https://example.com"))
    .header("Accept", "application/json")
    .timeout(Duration.ofSeconds(10))
    .GET()
    .build();

3. SQL Query Builders (JOOQ, Knex.js, QueryDSL)

query = db.select("name", "email")
    .from("users")
    .where("age", ">", 18)
    .orderBy("name", "ASC")
    .limit(10)
    .build();

4. Protocol Buffers (Google Protobuf)

All protobuf message classes use builders for construction - Person.newBuilder().setName("Alice").setId(123).build().

5. Pizza / Meal Builders (Educational)

Classic teaching example: Pizza.builder().size(LARGE).crust(THIN).topping(MUSHROOM).topping(OLIVE).build().

6. Configuration Objects (OkHttp, Retrofit, Spring)

OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(10, SECONDS)
    .readTimeout(30, SECONDS)
    .addInterceptor(loggingInterceptor)
    .cache(cache)
    .build();

7. Lombok @Builder (Java)

Annotation-based code generation that auto-creates a builder for any class - eliminates boilerplate entirely.


Advantages & Disadvantages

AdvantagesDisadvantages
Eliminates telescoping constructorsMore code (builder class + product class)
Self-documenting, readable APIExtra memory allocation for builder object
Enforces immutability naturallyCan be overkill for simple objects
Validates invariants at build timeDuplication of fields between builder and product
Same process → different representationsSlightly more complex debugging (extra indirection)
Isolates construction logic from business logicRequires updating builder when product changes
Supports partial construction and reuseNot natively supported in all languages without boilerplate
Thread-safe products (immutable)Builder itself is NOT thread-safe by default

Constraints & Edge Cases

Validation in build()

Always validate in build(), not in individual setter methods (unless fail-fast is critical). This allows the builder to be in temporarily inconsistent states during construction while guaranteeing the final product is valid.

method build(): Product:
    if requiredFieldA == null:
        throw IllegalStateException("fieldA is required")
    if fieldB < 0:
        throw IllegalArgumentException("fieldB must be non-negative")
    if fieldC != null and fieldD == null:
        throw IllegalStateException("fieldD required when fieldC is set")
    return new Product(this)

Required vs Optional Fields

  • Required fields → pass in the Builder's constructor (not optional setters)
  • Optional fields → provide sensible defaults; expose setter methods on builder

This makes it impossible to forget required fields - the compiler enforces them.

Thread Safety of the Builder

Builders are not thread-safe by default. A single builder instance should not be shared across threads during construction. If needed:

  • Create a new builder per thread
  • Or synchronize access (rarely justified - builders are short-lived)

The product can be safely shared across threads if it is immutable.

Builder Reuse

Can you call build() multiple times on the same builder? Two strategies:

  1. Reusable builder - each build() creates a new product from current state. Useful for creating similar objects with minor variations.
  2. Single-use builder - build() invalidates the builder (sets a flag, nulls internal state). Prevents accidental sharing of mutable state between products.

Document which strategy your builder follows.

Inheritance and Builders

When products form a hierarchy, use the Curiously Recurring Generic Pattern:

class Shape.Builder<T extends Builder<T>>:
    method color(c: Color): T
        this.color = c
        return self()
    abstract method self(): T

class Circle.Builder extends Shape.Builder<Circle.Builder>:
    method radius(r: double): Circle.Builder
        this.radius = r
        return this
    method self(): Circle.Builder
        return this

This preserves fluent chaining in subclass builders without casting.


Interview Follow-ups

Q1: How does the Builder pattern differ from the Abstract Factory pattern?

Answer: Abstract Factory focuses on creating families of related objects without specifying concrete classes - it returns finished products in one call. Builder focuses on constructing a single complex object step by step. Abstract Factory emphasizes what gets created (product families); Builder emphasizes how it gets created (construction process). You can combine them: an Abstract Factory might return a Builder for each product family.

Q2: How would you implement a Builder for an object with mandatory fields?

Answer: Pass mandatory fields as parameters to the Builder's constructor itself. This leverages compile-time safety - you cannot create a Builder without providing required values. Optional fields are set via chained methods with sensible defaults. The build() method performs cross-field validation for invariants that can't be enforced by the constructor alone.

Q3: Can the Builder pattern help with testing? How?

Answer: Yes, significantly. Builders make it easy to create test fixtures with only the relevant fields specified - all others use defaults. This reduces test setup noise. You can also create a TestDataBuilder that pre-fills fields with valid test data, letting each test override only what it cares about. This follows the Object Mother / Test Data Builder pattern.

Q4: How would you make a Builder thread-safe without synchronization?

Hint: Think about what makes the builder mutable and whether you can make each method return a new builder instance instead of mutating this. Consider the trade-off between allocation cost and safety.

Q5: How would you design a Builder that enforces a specific construction order (e.g., foundation before walls before roof)?

Hint: Consider using the type system - each step returns a different type that only exposes the next valid method. Look up "Step Builder" or "Staged Builder" pattern. Think about how interfaces can model state transitions at compile time.


Counter Questions to Ask Interviewer

Use these to clarify scope and demonstrate depth of thinking:

  1. "How many parameters does the object have, and how many are optional?" - Determines if Builder is justified or overkill.

  2. "Does the object need to be immutable after construction?" - Drives whether Builder is the right pattern vs. simple setters.

  3. "Will there be multiple representations of the same construction process?" - Determines if you need the Director + multiple ConcreteBuilders (GoF style) or a single fluent builder suffices.

  4. "Is there a required construction order, or can fields be set in any sequence?" - Determines if you need a Step Builder or a standard fluent builder.

  5. "Should the builder be reusable for creating multiple similar objects?" - Affects internal state management and whether build() resets or invalidates the builder.

  6. "Are there cross-field validation rules?" - Determines complexity of the build() method and whether you need a validation layer.


References & Whitepapers

  1. Gamma, E., Helm, R., Johnson, R., Vlissides, J. - Design Patterns: Elements of Reusable Object-Oriented Software (1994), Chapter 3: Creational Patterns - Builder. The original GoF formulation with Director/Builder separation.

  2. Bloch, Joshua - Effective Java, 3rd Edition (2018), Item 2: "Consider a builder when faced with many constructor parameters." The definitive modern treatment. Introduces the static inner Builder class pattern that became the industry standard in Java.

  3. Fowler, Martin - "Method Chaining" and "Fluent Interface" (martinfowler.com). Discusses the API design principles underlying modern builders.

  4. Freeman, S., Pryce, N. - Growing Object-Oriented Software, Guided by Tests (2009). Covers Test Data Builders extensively for creating test fixtures.

  5. Google Protocol Buffers Documentation - Real-world builder implementation at massive scale with code generation.

  6. Lombok Project - @Builder annotation documentation. Demonstrates compile-time builder generation eliminating boilerplate.



Builder is one of the most practically useful patterns in modern software engineering. While the GoF formulation with Director is valuable for complex construction orchestration, the Bloch-style fluent builder has become the de facto standard for everyday use. Master both forms - interviewers and production codebases expect fluency in each.