Builder Pattern

Builder Pattern

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

Intent

The Builder pattern addresses the telescoping constructor problem — when a class has many optional parameters, every combination requires a constructor overload. Instead, a Builder accumulates parameters step-by-step, then constructs the final Product via a build() call. An optional Director encapsulates a fixed construction sequence for repeated use.

When NOT to Use

  • Object has 3 or fewer parameters — a constructor is more explicit and compiler-checked
  • All parameters are required — Builder adds indirection without benefit; use a constructor with all required params
  • The product is simple and unlikely to evolve — Builder is overkill for stable simple objects
  • Performance is critical at construction time — Builder adds allocation and method call overhead vs a direct constructor

When to Use

  • Constructor would require 4+ parameters, especially with many optional ones (telescoping constructor smell)
  • Same construction steps must produce different representations (e.g., HTML document vs Markdown document)
  • Complex object must be built incrementally — some parts depend on previous parts
  • You want to enforce invariants: build() can validate that all required fields are set before returning the product

Suitability Threshold

When a constructor would require 4+ parameters, especially with optional ones, Builder is justified. Below that, use a constructor or named options object.

How It Works

Participants: Builder (interface with step methods), ConcreteBuilder (implements steps, holds product state), Director (optional — encapsulates a fixed step sequence), Product (the complex object).

The fluent interface (returning this from each step) is a modern variation that does not require a Director.

Class Diagram

classDiagram
    class Builder {
        <<interface>>
        +buildStepA()*
        +buildStepB()*
        +buildStepC()*
        +getResult()* Product
    }
    class ConcreteBuilder {
        -product : Product
        +buildStepA()
        +buildStepB()
        +buildStepC()
        +getResult() Product
    }
    class Director {
        -builder : Builder
        +construct()
    }
    class Product {
        +partA
        +partB
        +partC
    }
    Builder <|.. ConcreteBuilder
    Director o-- Builder : uses
    ConcreteBuilder ..> Product : builds

TypeScript Example

// Builder — TypeScript (fluent interface)
class QueryBuilder {
  private table = "";
  private conditions: string[] = [];
  private columns = "*";
  private limit?: number;
 
  from(table: string): this { this.table = table; return this; }
  select(cols: string): this { this.columns = cols; return this; }
  where(cond: string): this { this.conditions.push(cond); return this; }
  take(n: number): this { this.limit = n; return this; }
 
  build(): string {
    const where = this.conditions.length ? ` WHERE ${this.conditions.join(" AND ")}` : "";
    const limit = this.limit != null ? ` LIMIT ${this.limit}` : "";
    return `SELECT ${this.columns} FROM ${this.table}${where}${limit}`;
  }
}
 
// Director is optional — useful when the same fixed sequence is reused
class ReportQueryDirector {
  construct(builder: QueryBuilder): string {
    return builder.from("orders").select("id, total").where("status = 'active'").take(100).build();
  }
}
 
// Usage without Director
const query = new QueryBuilder().from("users").select("name, email").where("active = true").build();
 
// Usage with Director
const reportQuery = new ReportQueryDirector().construct(new QueryBuilder());

Java Example

// Builder — Java (fluent interface)
public class QueryBuilder {
    private String table = "";
    private List<String> conditions = new ArrayList<>();
    private String columns = "*";
    private Integer limit;
 
    public QueryBuilder from(String table) { this.table = table; return this; }
    public QueryBuilder select(String cols) { this.columns = cols; return this; }
    public QueryBuilder where(String cond) { this.conditions.add(cond); return this; }
    public QueryBuilder take(int n) { this.limit = n; return this; }
 
    public String build() {
        String where = conditions.isEmpty() ? "" : " WHERE " + String.join(" AND ", conditions);
        String lim = limit != null ? " LIMIT " + limit : "";
        return "SELECT " + columns + " FROM " + table + where + lim;
    }
}
 
// Usage
String query = new QueryBuilder().from("users").select("name, email").where("active = true").build();
ConceptRelationship
Factory-Method-PatternFactory Method selects which class to instantiate; Builder constructs one complex object step-by-step
Abstract-Factory-PatternAbstract Factory creates a product family in one call; Builder assembles one product incrementally
Prototype-PatternPrototype clones existing instances; Builder constructs from scratch
Singleton-PatternDirector is sometimes a Singleton (use DI-managed instance instead)

Sources

  • Gamma et al., Design Patterns, 1994, pp. 97–106 — authoritative definition and Director role
  • Bloch, Effective Java 3rd ed., Item 2 — Java telescoping constructor problem and Builder solution
  • refactoring.guru/design-patterns/builder — modern fluent builder framing