designpattern.site

Builder Pattern

Construct complex objects step by step, allowing the same construction process to create different representations.

Overview

The Builder pattern is a creational design pattern that separates the construction of a complex object from its representation. The same construction process can produce different results depending on which builder you use.

Think of it like ordering a custom sandwich. You don't hand the server a sandwich and say "make it like this one." Instead, you specify each component in sequence: bread, protein, toppings, sauce. The process is the same every time, but the result varies based on your choices.

Builder is especially valuable when an object requires many optional parameters. Without it, you end up with constructors that take a dozen arguments — most of which are null or false on any given call.

The Problem

Consider building a House object. A house might have walls, doors, windows, a roof, a garage, a swimming pool, a garden, and multiple floors. Most houses only have a subset of these features.

The naive approach is a constructor with every possible parameter:

// The "telescoping constructor" anti-pattern
const house = new House(4, 2, 8, true, false, true, false, 2);
// What does each argument mean? Impossible to tell at a glance.

This is called the telescoping constructor anti-pattern. As optional parameters multiply, so do constructor overloads or nullable arguments. Callers must remember the exact parameter order and meaning of each position. A single misplaced true silently builds the wrong object.

You could switch to a plain object or named parameters, but that loses any validation logic and leaves the object in a potentially inconsistent state mid-construction.

The Solution

The Builder pattern introduces a dedicated Builder object that assembles the product step by step. Each step is a method call with a meaningful name. When construction is complete, you call a terminal method (build() or getResult()) to receive the finished product.

An optional Director class can encode common construction recipes — "build a minimal house," "build a luxury house" — so callers don't need to repeat the same step sequences.

Class Diagram

Loading diagram...

The Director holds a reference to any Builder and calls its methods in a fixed order. Swap in a different concrete builder to get a different product from the same Director method.

Implementation

The following example builds SQL queries step by step. A QueryBuilder accumulates optional clauses (WHERE, ORDER BY, LIMIT) and emits a final SQL string via build(). All four implementations solve the same problem and produce equivalent output.

class QueryBuilder {
  private table: string;
  private conditions: string[] = [];
  private orderByClause: string | null = null;
  private limitClause: number | null = null;

  constructor(table: string) {
    this.table = table;
  }

  where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }

  orderBy(column: string, direction: "ASC" | "DESC" = "ASC"): this {
    this.orderByClause = `${column} ${direction}`;
    return this;
  }

  limit(n: number): this {
    this.limitClause = n;
    return this;
  }

  build(): string {
    let query = `SELECT * FROM ${this.table}`;
    if (this.conditions.length > 0) {
      query += ` WHERE ${this.conditions.join(" AND ")}`;
    }
    if (this.orderByClause) {
      query += ` ORDER BY ${this.orderByClause}`;
    }
    if (this.limitClause !== null) {
      query += ` LIMIT ${this.limitClause}`;
    }
    return query;
  }
}

// Usage
const query = new QueryBuilder("users")
  .where("age > 18")
  .where("active = true")
  .orderBy("created_at", "DESC")
  .limit(10)
  .build();

console.log(query);
// SELECT * FROM users WHERE age > 18 AND active = true ORDER BY created_at DESC LIMIT 10

Director vs. No Director

The Director is optional

The Director class is a convenience, not a requirement. You can call Builder methods directly from client code to get full control over the construction sequence. Use a Director when you want to reuse a fixed recipe across multiple call sites — for example, always building a "default configuration" the same way. Omit it when each call site needs a different combination of steps.

Real-World Examples

SQL query builders — Libraries like Knex.js (Node), SQLAlchemy Core (Python), and jOOQ (Java) use the Builder pattern to compose queries from optional clauses. The same builder API generates syntactically valid SQL regardless of which clauses you add.

HTTP request builders — The fetch API options object is a lightweight builder. More explicit builder APIs appear in libraries like axios, Java's HttpRequest.newBuilder(), and Go's http.NewRequest. Each step adds headers, sets the method, or attaches a body.

Test data builders — In unit tests, a UserBuilder or OrderBuilder sets sensible defaults and lets each test override only the fields it cares about. This reduces noise in test setup and makes intent clear:

const user = new UserBuilder()
  .withEmail("test@example.com")
  .withRole("admin")
  .build();

Document generation — PDF and HTML document libraries (ReportLab, iText, docx) use builders to compose pages, sections, and styling in sequence before rendering the final file.

Configuration objects — Server frameworks expose configuration builders: ServerBuilder().withPort(8080).withTLS(cert, key).withTimeout(30).build(). This avoids large configuration structs where most fields are irrelevant.

Pros and Cons

Pros

  • Eliminates telescoping constructors — each parameter is named at the call site.
  • Supports step-by-step construction and validation between steps.
  • Reuse the same construction process with different builders to produce different representations.
  • Director classes encapsulate common build recipes, reducing duplication.

Cons

  • Adds several new classes (Builder interface, concrete builders, optional Director) even for simple objects.
  • The product is not available until build() is called, which can feel awkward if you need partial results.
  • Fluent chaining can obscure the order in which steps must be called when ordering matters.

When to Use

Use Builder when:

  • An object has many optional parameters and you want to avoid positional constructor arguments.
  • Construction requires multiple steps that must execute in a specific order.
  • You want to produce different representations of the same structure (wooden house vs. stone house, PDF vs. HTML).
  • Object construction involves validation or transformation logic that shouldn't live in the constructor.

Avoid Builder when:

  • The object is simple and has only a few mandatory fields — a plain constructor or factory function is cleaner.
  • The language already provides named parameters or keyword arguments (Python, Kotlin) — those can often replace the pattern entirely.

Abstract Factory also creates objects without specifying their concrete classes, but it focuses on creating families of related objects all at once rather than assembling a single complex object step by step.

Composite trees are a natural fit for Builder: the Director can orchestrate building a Composite structure by adding leaves and branches in sequence.

Factory Method delegates instantiation to a subclass but does not manage multi-step construction. Use Factory Method when the creation logic is simple; reach for Builder when the object requires a construction sequence.

On this page