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:
-
Builder (interface or base class): declares the
configuration methods for each construction step. E.g.:
setUrl(),setMethod(),addHeader(),build(). -
ConcreteBuilder: implements the steps, accumulates
the intermediate state and provides the
build()method that validates and returns the product. In TypeScript, each step returnsthisfor fluent chaining. - 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.
-
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.
}
<?php
// ── Product (immutable via readonly) ──────────────────────────
final class HttpRequest
{
/** @param array<string, string> $headers */
public function __construct(
public readonly string $url,
public readonly string $method,
public readonly array $headers,
public readonly ?string $body = null,
) {}
}
// ── Builder ───────────────────────────────────────────────────
class HttpRequestBuilder
{
private string $url = '';
private string $method = 'GET';
/** @var array<string, string> */
private array $headers = [];
private ?string $body = null;
public function setUrl(string $url): static
{
$this->url = $url;
return $this;
}
public function setMethod(string $method): static
{
$this->method = $method;
return $this;
}
public function addHeader(string $key, string $value): static
{
$this->headers[$key] = $value;
return $this;
}
public function setBody(string $body): static
{
$this->body = $body;
return $this;
}
// Centralized validation: happens once, before creating the product.
public function build(): HttpRequest
{
if ($this->url === '') {
throw new \InvalidArgumentException('HttpRequest: url is required.');
}
if (in_array($this->method, ['POST', 'PUT', 'PATCH'], true) && $this->body === null) {
throw new \InvalidArgumentException(
"HttpRequest: body is required for {$this->method}."
);
}
return new HttpRequest($this->url, $this->method, $this->headers, $this->body);
}
}
// ── Usage ────────────────────────────────────────────────────
$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();
echo $req->method . PHP_EOL; // POST
echo $req->url . PHP_EOL; // https://api.example.com/users
// Attempting an invalid build:
try {
(new HttpRequestBuilder())
->setUrl('https://api.example.com/users')
->setMethod('POST')
->build(); // Missing body!
} catch (\InvalidArgumentException $e) {
echo $e->getMessage() . PHP_EOL;
// → 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
<?php
// Director: knows the canonical construction sequences.
class ApiClientDirector
{
public function __construct(
private readonly HttpRequestBuilder $builder,
) {}
// Builds a standard authenticated GET request.
public function buildAuthenticatedGet(string $url, string $token): 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.
public function buildJsonPost(string $url, string $token, array $payload): HttpRequest
{
return $this->builder
->setUrl($url)
->setMethod('POST')
->addHeader('Authorization', "Bearer {$token}")
->addHeader('Content-Type', 'application/json')
->addHeader('Accept', 'application/json')
->setBody(json_encode($payload))
->build();
}
}
// ── Usage ────────────────────────────────────────────────────
$director = new ApiClientDirector(new HttpRequestBuilder());
$getReq = $director->buildAuthenticatedGet(
'https://api.example.com/profile',
'abc123',
);
echo "{$getReq->method} {$getReq->url}" . PHP_EOL;
// → GET https://api.example.com/profile
$postReq = $director->buildJsonPost(
'https://api.example.com/orders',
'abc123',
['product' => 'Notebook', 'quantity' => 2],
);
echo "{$postReq->method} {$postReq->url}" . PHP_EOL;
// → 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
thisin 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.