Coding · Design patterns
Builder pattern explained with examples
Short answer
Builder is a creational design pattern that separates the construction of a complex object from its final representation, so the same step-by-step process can create different results. In modern code it usually appears as a fluent API: you call methods such as .url(), .header() and .timeout() on a builder, then .build() returns a finished, validated object. It replaces long constructors with many optional parameters.
Intent
Separate the construction of a complex object from its representation so that the same construction process can create different representations. Builder is one of the five creational patterns in the Gang of Four (GoF) book.
The problem it solves
Imagine an HTTP request object with a URL, a method, any number of headers, an optional body, a timeout and a retry count. A constructor for all of this is hard to call correctly:
class HttpRequest {
constructor(
readonly url: string,
readonly method: string,
readonly headers: Record<string, string>,
readonly body?: string,
readonly timeoutMs?: number,
readonly retries?: number,
) {}
}
// Which argument is which? Easy to swap 3 and 5000 by mistake.
const req = new HttpRequest("https://api.example.com", "POST", {}, undefined, 3, 5000);This is called a telescoping constructor. It also has nowhere to put rules such as "a GET request cannot have a body", and it cannot be built gradually, for example when headers are added in several places before sending.
Structure and participants
| Participant | Responsibility | In the example |
|---|---|---|
| Builder | Declares the steps for creating parts of the product. | RequestBuilder methods |
| ConcreteBuilder | Implements the steps, keeps the partial state and returns the result. | RequestBuilder |
| Director | Optional. Runs the steps in a fixed order to produce common configurations. | jsonPost() |
| Product | The complex object being built. | HttpRequest |
TypeScript implementation
Each step stores a value and returns this, so calls can be chained. build() validates the combination and returns an immutable product:
interface HttpRequest {
readonly url: string;
readonly method: "GET" | "POST";
readonly headers: Readonly<Record<string, string>>;
readonly body?: string;
readonly timeoutMs: number;
}
class RequestBuilder {
private method: "GET" | "POST" = "GET";
private headers: Record<string, string> = {};
private body?: string;
private timeoutMs = 10_000;
constructor(private readonly url: string) {}
post(body: string): this { this.method = "POST"; this.body = body; return this; }
header(name: string, value: string): this { this.headers[name] = value; return this; }
timeout(ms: number): this { this.timeoutMs = ms; return this; }
build(): HttpRequest {
if (this.timeoutMs <= 0) throw new Error("timeout must be positive");
return Object.freeze({
url: this.url, method: this.method, headers: { ...this.headers },
body: this.body, timeoutMs: this.timeoutMs,
});
}
}
const request = new RequestBuilder("https://api.example.com/orders")
.header("Accept", "application/json")
.timeout(5_000)
.build();
console.log(request.method, request.timeoutMs, request.headers);
// GET 5000 { Accept: 'application/json' }A Director is just code that knows a common recipe. In TypeScript it is often a plain function that drives the builder:
interface Steps {
header(name: string, value: string): this;
post(body: string): this;
}
// Director: a reusable recipe that drives any builder with these steps.
function jsonPost<B extends Steps>(builder: B, data: unknown): B {
return builder
.header("Content-Type", "application/json")
.post(JSON.stringify(data));
}
class LogBuilder implements Steps {
log: string[] = [];
header(name: string, value: string): this { this.log.push(`${name}=${value}`); return this; }
post(body: string): this { this.log.push(`POST ${body}`); return this; }
}
console.log(jsonPost(new LogBuilder(), { id: 7 }).log);
// [ 'Content-Type=application/json', 'POST {"id":7}' ]This shows the original GoF idea: the same construction steps, driven by the same director, can feed different builders and produce different representations, here a log instead of a request.
When to use it, and when not to
Use Builder when:
- An object has many optional parts, or parts that must be added one at a time.
- Construction has rules that involve several fields and should be checked once, in
build(). - You want the finished object to be immutable, while still assembling it step by step.
- The same construction process must produce different representations, such as a document exported as HTML, Markdown or PDF.
Avoid it when:
- The object has a few fields. A constructor is shorter and just as clear.
- Your language already has named or optional parameters. In TypeScript, a single options object often removes the need for a builder entirely.
interface RequestOptions {
url: string;
method?: "GET" | "POST";
headers?: Record<string, string>;
timeoutMs?: number;
}
function createRequest({ url, method = "GET", headers = {}, timeoutMs = 10_000 }: RequestOptions) {
return { url, method, headers, timeoutMs };
}
console.log(createRequest({ url: "/orders", timeoutMs: 5_000 }).method); // GETReal-world uses
- Java:
java.net.http.HttpRequest.newBuilder()returns anHttpRequest.Builder; you chainuri(),header()andtimeout(), then callbuild(). - Java:
Locale.Builderassembles and validates a locale from language, region and script parts. - Java and C#:
StringBuilderassembles a string from many appended parts before producing the final immutable string. - Many SQL query builders and test-data builders use the same fluent style.
Related patterns
- Abstract Factory creates whole families of products and returns each one immediately; Builder constructs one complex product step by step and returns it at the end.
- Composite trees are often created with a builder, because building a tree node by node is a natural series of steps.
- Fluent interface is the API style of chaining calls that return
this. Builder often uses it, but they are not the same thing. - Prototype can be an alternative when most new objects are small variations of a ready-made template.
FAQ
Why does each builder method return this?
Returning the builder lets you chain calls in one expression, such as .header(...).timeout(...).build(). Returning this in TypeScript also keeps the correct type in subclasses.
Do I need a Director class?
No. The Director is optional in the GoF description. In practice, a function that applies a common sequence of steps is usually enough.
Is Builder still useful in TypeScript?
Sometimes. An options object with defaults covers most simple cases. Builder is still useful when objects are assembled gradually, when cross-field validation is needed, or when you want an immutable result.
What is the difference between Builder and Factory Method?
Factory Method decides which class to instantiate and returns the object in one call. Builder lets the client control how one object is assembled over several calls.