Creational Pattern (GoF)

Builder

Separates the construction of a complex object from its representation, allowing the same construction process to create different representations and guaranteeing that the product is only delivered in a valid state.

Intent

Separate the assembly process of a complex object from its final representation, allowing the same construction process to produce different representations. Builder encapsulates each step in individual methods and centralizes all validation in the build() method, which only delivers the product once the configuration is complete and consistent.

Cataloged by Gamma, Helm, Johnson and Vlissides in the book Design Patterns: Elements of Reusable Object-Oriented Software (1994), Builder belongs to the creational patterns category. It's especially valuable when the final object has many optional parameters, when construction order matters, or when validation needs to be centralized before the product is delivered.

Problem

Objects with many parameters — some required, some optional, with interdependencies — generate two classic problems when built through a constructor or public setters:

  • Telescoping constructor: to cover every combination of optional parameters, you create multiple overloads (new Order(items), new Order(items, coupon), new Order(items, coupon, shipping)…). The code that instantiates the object becomes unreadable and error-prone regarding parameter order.
  • Object in an inconsistent state: with public setters, the object can be used before it's fully configured. Premature validation (in the individual setter) can't see the full state; late validation (in the usage method) is too far from the creation point.

Builder solves both: each parameter becomes a step method with a descriptive name, and full validation happens in build().

Builder vs Abstract Factory

The most important distinction — and the one that confuses the most:

  • Abstract Factory creates families of related objects instantly. The focus is on family consistency of products.
  • Builder constructs a single complex object step by step. The focus is on the assembly process and on centralized validation before delivering the product.

In short: Abstract Factory is about what to create (which family); Builder is about how to build (which process, with which constraints).

Solution

Builder organizes the code into four participants:

  1. Builder (interface or base class): declares the configuration methods for each construction step. E.g.: setUrl(), setMethod(), addHeader(), build().
  2. ConcreteBuilder: implements the steps, accumulates the intermediate state and provides the build() method that validates and returns the product. In TypeScript, each step returns this for fluent chaining.
  3. Director (optional): encapsulates common construction sequences, calling the builder's steps in a predefined order. Useful when the same canonical configurations appear in multiple places.
  4. Product: the resulting complex object. Frequently immutable — after build(), no configuration method can alter it anymore.

The secret of the pattern lies in build(): that's where centralized validation happens. Before build(), the builder is mutable and tolerant; afterwards, the product is immutable and guaranteed to be valid.

Structure

Simplified UML diagram with an HTTP request builder example:

          HttpRequest (Product — immutable)
┌──────────────────────────────────────────────────────┐
│ + readonly url: string                               │
│ + readonly method: "GET"|"POST"|"PUT"|"PATCH"|"DEL." │
│ + readonly headers: Record<string, string>           │
│ + readonly body?: string                             │
└──────────────────────────────────────────────────────┘
                         ▲
                         │ creates
 HttpRequestBuilder (ConcreteBuilder)
┌────────────────────────────────────┐
│ - _url: string                     │
│ - _method: Method                  │
│ - _headers: Record<string, string> │
│ - _body?: string                   │
├────────────────────────────────────┤
│ + setUrl(url): this                │  ← fluent (returns this)
│ + setMethod(m): this               │
│ + addHeader(k, v): this            │
│ + setBody(b): this                 │
│ + build(): HttpRequest             │  ← validates and returns
└────────────────────────────────────┘

Construction flow:

  const req = new HttpRequestBuilder()
    .setUrl("https://api.example.com/users")
    .setMethod("POST")
    .addHeader("Content-Type", "application/json")
    .setBody('{"name":"Ana"}')
    .build();               // ← centralized validation happens here

Code examples

Example 1 — HTTP request builder with validation in build()

The builder accumulates the configuration step by step. The build() method validates the complete state and creates the immutable product — preventing invalid requests from existing in the system.

type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

// ── Product (immutable) ────────────────────────────────────────
class HttpRequest {
  constructor(
    public readonly url: string,
    public readonly method: Method,
    public readonly headers: Readonly<Record<string, string>>,
    public readonly body?: string,
  ) {}
}

// ── Builder ───────────────────────────────────────────────────
class HttpRequestBuilder {
  private _url: string = "";
  private _method: Method = "GET";
  private _headers: Record<string, string> = {};
  private _body?: string;

  setUrl(url: string): this {
    this._url = url;
    return this;
  }

  setMethod(method: Method): this {
    this._method = method;
    return this;
  }

  addHeader(key: string, value: string): this {
    this._headers[key] = value;
    return this;
  }

  setBody(body: string): this {
    this._body = body;
    return this;
  }

  // Centralized validation: happens once, before creating the product.
  build(): HttpRequest {
    if (!this._url) {
      throw new Error("HttpRequest: url is required.");
    }
    if (["POST", "PUT", "PATCH"].includes(this._method) && !this._body) {
      throw new Error(`HttpRequest: body is required for ${this._method}.`);
    }
    return new HttpRequest(
      this._url,
      this._method,
      { ...this._headers },
      this._body,
    );
  }
}

// ── Usage ────────────────────────────────────────────────────
const req = new HttpRequestBuilder()
  .setUrl("https://api.example.com/users")
  .setMethod("POST")
  .addHeader("Content-Type", "application/json")
  .addHeader("Authorization", "Bearer token123")
  .setBody('{"name":"Ana","email":"ana@example.com"}')
  .build();

console.log(req.method); // "POST"
console.log(req.url);    // "https://api.example.com/users"

// Attempting an invalid build:
try {
  new HttpRequestBuilder()
    .setUrl("https://api.example.com/users")
    .setMethod("POST")
    .build(); // Missing body!
} catch (e) {
  console.error((e as Error).message);
  // → HttpRequest: body is required for POST.
}

Example 2 — Director: reusable construction sequences

The Director encapsulates canonical construction sequences, freeing the client code from repeating the same steps in multiple places. The Director doesn't know which concrete builder it's using — it works through the interface, not the implementation.

// Director: knows the canonical construction sequences.
class ApiClientDirector {
  constructor(private readonly builder: HttpRequestBuilder) {}

  // Builds a standard authenticated GET request.
  buildAuthenticatedGet(url: string, token: string): HttpRequest {
    return this.builder
      .setUrl(url)
      .setMethod("GET")
      .addHeader("Authorization", `Bearer ${token}`)
      .addHeader("Accept", "application/json")
      .build();
  }

  // Builds a standard authenticated POST request with JSON.
  buildJsonPost(url: string, token: string, payload: unknown): HttpRequest {
    return this.builder
      .setUrl(url)
      .setMethod("POST")
      .addHeader("Authorization", `Bearer ${token}`)
      .addHeader("Content-Type", "application/json")
      .addHeader("Accept", "application/json")
      .setBody(JSON.stringify(payload))
      .build();
  }
}

// ── Usage ────────────────────────────────────────────────────
const director = new ApiClientDirector(new HttpRequestBuilder());

const getReq = director.buildAuthenticatedGet(
  "https://api.example.com/profile",
  "abc123",
);
console.log(`${getReq.method} ${getReq.url}`);
// → GET https://api.example.com/profile

const postReq = director.buildJsonPost(
  "https://api.example.com/orders",
  "abc123",
  { product: "Notebook", quantity: 2 },
);
console.log(`${postReq.method} ${postReq.url}`);
// → POST https://api.example.com/orders

When to use

  • Objects with many optional parameters: when a constructor starts having 4 or more parameters — especially with several optional ones — Builder makes the client code readable and resistant to ordering mistakes.
  • When centralized validation matters: build() is the single point where all rules are checked together, guaranteeing that the product only exists in a valid state.
  • When the product must be immutable: Builder collects the mutable configuration and delivers an immutable product — a classic pattern for value objects and DTOs.
  • When construction order matters: the Director encapsulates the order, guaranteeing that required steps are never forgotten.

When to avoid

  • Simple objects with few fixed parameters: 2–3 required parameters without optional ones don't justify an intermediate builder class.
  • When there's no real validation in build(): if build() just instantiates without validating anything, Builder is only syntactic sugar over chained setters — assess whether the readability gain justifies the extra class.
  • When the product's immutability doesn't matter: if the object needs to be mutable after creation anyway, the pattern loses part of its main appeal.

Pros and cons

Pros

  • Eliminates the telescoping constructor: each optional parameter gets its own descriptively named method.
  • Centralized validation in build(): the product only exists if it's valid — impossible to have an inconsistent object state.
  • Supports immutable products: the builder is mutable during configuration; the delivered product is immutable.
  • Readable at the call site: .setUrl(...).setMethod("POST").addHeader(...).build() documents intent better than a positional list of parameters.
  • The Director allows reusing common construction sequences without duplication.

Cons

  • More code: every product requires an equivalent builder class — overhead for simple objects.
  • The client can call build() forgetting required steps — validation catches it at runtime, but not at compile time.
  • Fluent chaining with this in TypeScript can cause surprises with subclasses; the return type needs care.

Common pitfalls

1. Builder without validation in build() — just fluent setters

Warning: The most common mistake is creating chained methods that just copy values into fields, and a build() that instantiates without validating anything. That's a fluent API over setters — not the Builder pattern. The essence of the pattern is centralized validation and a guaranteed-valid product. Without that, you have extra complexity without the main benefit.

2. Reusing the builder after build()

Calling configuration methods after build() and calling build() again can produce products with an unexpected state, especially when the builder accumulates items (like headers). Two common strategies: (a) throw an exception if build() is called twice, or (b) reset the internal state after each build(), making the builder explicitly reusable.

3. Confusing Builder with Abstract Factory

Abstract Factory delivers ready objects from a family in a single call. Builder assembles a single complex object step by step with validation. If you need to guarantee that button and checkbox are from the same theme, use Abstract Factory. If you need to configure an HTTP request with many optional headers and validate everything before sending it, use Builder.

4. Unnecessary Director

The Director is optional. If there are no construction sequences reused in multiple places, the client can call the builder's steps directly without the extra layer. Introduce the Director only when the same sequence appears in three or more places in the code — DRY applied to construction.

Related patterns

Builder interacts with other creational and structural patterns:

Abstract Factory is frequently compared to Builder: both are creational patterns, but Abstract Factory creates families of simple objects at once, while Builder assembles a single complex object step by step with centralized validation. Prototype can be used inside a builder when creation starts from a cloned base configuration — the builder clones the prototype and applies customizations on top. Composite is frequently the product being built by Builder: tree structures (documents, pipelines, UI trees) are natural candidates for step-by-step construction.