Builder Pattern Explained

Introduction
Imagine you need to construct a complex object — a User with 10 optional fields, an HTTP request with headers, query params, a body, and timeout settings, or a SQL query with joins, where clauses, and ordering.
How do you create such objects without writing monstrous constructors or polluting your code with a sea of null arguments?
The Builder Pattern solves exactly this problem. It separates the construction of a complex object from its representation, allowing the same construction process to produce different results.
What You'll Learn
✅ Understand the problem Builder pattern solves
✅ Implement Builder with fluent interface (method chaining)
✅ Use the optional Director component
✅ Apply Builder in TypeScript, Python, and Java
✅ Recognize real-world Builder patterns (query builders, HTTP clients)
✅ Know when to use Builder vs other creational patterns
Prerequisites
- Completed Factory and Abstract Factory Patterns
- Familiar with SOLID Principles
- Comfortable with classes and interfaces (Classes, Objects, and Abstraction)
The Problem: Telescoping Constructors
Consider a User class with many optional fields:
// ❌ Telescoping constructor anti-pattern
class User {
constructor(
name: string,
email: string,
age?: number,
phone?: string,
address?: string,
city?: string,
country?: string,
zipCode?: string,
profilePicture?: string,
bio?: string
) { ... }
}
// Calling it is a nightmare
const user1 = new User("Alice", "alice@example.com");
const user2 = new User("Bob", "bob@example.com", 30, null, null, null, null, null, null, "Developer");
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ what are these?Problems with this approach:
- Unreadable call sites — what does the 5th
nullmean? - Order-dependent — easy to swap arguments by mistake
- Combinatorial explosion — 10 optional fields = many overloaded constructors
- Hard to extend — adding a field breaks all existing call sites
The Object Literal Workaround (and its limits)
// Slightly better — named fields
const user = new User({
name: "Alice",
email: "alice@example.com",
bio: "Developer"
});This works for simple cases, but loses validation (you can't validate step-by-step), immutability (the config object is mutable), and fluent readability (you can't chain calls to express a pipeline).
The Builder pattern solves all of these.
The Builder Pattern
Intent
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
Structure
Key Components
| Component | Role |
|---|---|
| Builder (interface) | Declares construction steps |
| ConcreteBuilder | Implements construction steps, holds partial result |
| Product | The complex object being built |
| Director (optional) | Orchestrates building steps in a specific order |
Implementation
TypeScript
// ✅ Product - the complex object
class User {
private constructor(
public readonly name: string,
public readonly email: string,
public readonly age?: number,
public readonly phone?: string,
public readonly address?: string,
public readonly bio?: string
) {}
// Static method to get a builder
static builder(): UserBuilder {
return new UserBuilder();
}
toString(): string {
return `User(name=${this.name}, email=${this.email}, age=${this.age ?? 'N/A'})`;
}
}
// ✅ Builder - fluent interface with method chaining
class UserBuilder {
private _name!: string;
private _email!: string;
private _age?: number;
private _phone?: string;
private _address?: string;
private _bio?: string;
setName(name: string): UserBuilder {
this._name = name;
return this; // Enable chaining
}
setEmail(email: string): UserBuilder {
this._email = email;
return this;
}
setAge(age: number): UserBuilder {
if (age < 0 || age > 150) {
throw new Error(`Invalid age: ${age}`);
}
this._age = age;
return this;
}
setPhone(phone: string): UserBuilder {
this._phone = phone;
return this;
}
setAddress(address: string): UserBuilder {
this._address = address;
return this;
}
setBio(bio: string): UserBuilder {
this._bio = bio;
return this;
}
build(): User {
// Validate required fields
if (!this._name) throw new Error("Name is required");
if (!this._email) throw new Error("Email is required");
// Private constructor - only builder can create User
return new (User as any)(
this._name,
this._email,
this._age,
this._phone,
this._address,
this._bio
);
}
}
// ✅ Usage - fluent, readable, self-documenting
const minimalUser = User.builder()
.setName("Alice")
.setEmail("alice@example.com")
.build();
const fullUser = User.builder()
.setName("Bob")
.setEmail("bob@example.com")
.setAge(30)
.setPhone("+1-555-0100")
.setAddress("123 Main St, New York")
.setBio("Full-stack developer with 5 years experience")
.build();
console.log(minimalUser.toString()); // User(name=Alice, email=alice@example.com, age=N/A)
console.log(fullUser.name); // BobThe Director (Optional)
The Director encapsulates common construction recipes:
// ✅ Director - optional component for preset configurations
class UserDirector {
private builder: UserBuilder;
constructor(builder: UserBuilder) {
this.builder = builder;
}
// Build a guest user with minimal info
buildGuestUser(name: string): User {
return this.builder
.setName(name)
.setEmail(`${name.toLowerCase().replace(' ', '.')}@guest.temp`)
.build();
}
// Build a full admin user
buildAdminUser(name: string, email: string): User {
return this.builder
.setName(name)
.setEmail(email)
.setAge(25)
.setBio("System Administrator")
.build();
}
}
const director = new UserDirector(User.builder());
const guest = director.buildGuestUser("Guest User");
const admin = director.buildAdminUser("Admin", "admin@example.com");Python
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class User:
"""Product - the complex object being constructed."""
name: str
email: str
age: Optional[int] = None
phone: Optional[str] = None
address: Optional[str] = None
bio: Optional[str] = None
def __repr__(self) -> str:
return f"User(name={self.name!r}, email={self.email!r}, age={self.age!r})"
class UserBuilder:
"""Builder - constructs User objects step by step."""
def __init__(self) -> None:
self._name: Optional[str] = None
self._email: Optional[str] = None
self._age: Optional[int] = None
self._phone: Optional[str] = None
self._address: Optional[str] = None
self._bio: Optional[str] = None
def set_name(self, name: str) -> UserBuilder:
self._name = name
return self # Enable chaining
def set_email(self, email: str) -> UserBuilder:
self._email = email
return self
def set_age(self, age: int) -> UserBuilder:
if not 0 <= age <= 150:
raise ValueError(f"Invalid age: {age}")
self._age = age
return self
def set_phone(self, phone: str) -> UserBuilder:
self._phone = phone
return self
def set_address(self, address: str) -> UserBuilder:
self._address = address
return self
def set_bio(self, bio: str) -> UserBuilder:
self._bio = bio
return self
def build(self) -> User:
if not self._name:
raise ValueError("Name is required")
if not self._email:
raise ValueError("Email is required")
return User(
name=self._name,
email=self._email,
age=self._age,
phone=self._phone,
address=self._address,
bio=self._bio,
)
# Usage - fluent and readable
minimal_user = (
UserBuilder()
.set_name("Alice")
.set_email("alice@example.com")
.build()
)
full_user = (
UserBuilder()
.set_name("Bob")
.set_email("bob@example.com")
.set_age(30)
.set_phone("+1-555-0100")
.set_address("123 Main St, New York")
.set_bio("Full-stack developer")
.build()
)
print(minimal_user) # User(name='Alice', email='alice@example.com', age=None)
print(full_user.name) # BobJava
public class User {
// Immutable fields - set only via builder
private final String name;
private final String email;
private final Integer age;
private final String phone;
private final String address;
private final String bio;
// Private constructor - only Builder can create User
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
this.bio = builder.bio;
}
// Getters
public String getName() { return name; }
public String getEmail() { return email; }
public Integer getAge() { return age; }
public String getPhone() { return phone; }
public String getAddress() { return address; }
public String getBio() { return bio; }
@Override
public String toString() {
return "User(name=" + name + ", email=" + email + ", age=" + age + ")";
}
// Static inner Builder class
public static class Builder {
// Required fields
private final String name;
private final String email;
// Optional fields
private Integer age;
private String phone;
private String address;
private String bio;
public Builder(String name, String email) {
if (name == null || name.isEmpty()) throw new IllegalArgumentException("Name required");
if (email == null || email.isEmpty()) throw new IllegalArgumentException("Email required");
this.name = name;
this.email = email;
}
public Builder age(int age) {
if (age < 0 || age > 150) throw new IllegalArgumentException("Invalid age: " + age);
this.age = age;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder bio(String bio) {
this.bio = bio;
return this;
}
public User build() {
return new User(this);
}
}
}
// Usage
User minimalUser = new User.Builder("Alice", "alice@example.com")
.build();
User fullUser = new User.Builder("Bob", "bob@example.com")
.age(30)
.phone("+1-555-0100")
.address("123 Main St, New York")
.bio("Full-stack developer")
.build();
System.out.println(minimalUser); // User(name=Alice, email=alice@example.com, age=null)
System.out.println(fullUser.getName()); // BobNote on Java style: The Java approach uses a static inner
Builderclass — this is the conventional Java idiom (popularized by Joshua Bloch's Effective Java). Required fields go in theBuilderconstructor; optional ones are chained setters.
Real-World Examples
1. HTTP Request Builder
A common real-world use case — building HTTP requests with many optional parts:
class HttpRequest {
private constructor(
public readonly method: string,
public readonly url: string,
public readonly headers: Record<string, string>,
public readonly body?: string,
public readonly timeout: number = 5000,
public readonly retries: number = 0
) {}
static builder(method: string, url: string): HttpRequestBuilder {
return new HttpRequestBuilder(method, url);
}
}
class HttpRequestBuilder {
private _headers: Record<string, string> = {};
private _body?: string;
private _timeout = 5000;
private _retries = 0;
constructor(
private readonly _method: string,
private readonly _url: string
) {}
header(key: string, value: string): HttpRequestBuilder {
this._headers[key] = value;
return this;
}
bearerToken(token: string): HttpRequestBuilder {
return this.header("Authorization", `Bearer ${token}`);
}
jsonBody(data: object): HttpRequestBuilder {
this._body = JSON.stringify(data);
return this.header("Content-Type", "application/json");
}
timeout(ms: number): HttpRequestBuilder {
this._timeout = ms;
return this;
}
retries(count: number): HttpRequestBuilder {
this._retries = count;
return this;
}
build(): HttpRequest {
return new (HttpRequest as any)(
this._method,
this._url,
this._headers,
this._body,
this._timeout,
this._retries
);
}
}
// ✅ Readable, explicit, extendable
const request = HttpRequest.builder("POST", "https://api.example.com/users")
.bearerToken("abc123")
.jsonBody({ name: "Alice", role: "admin" })
.timeout(10_000)
.retries(3)
.build();2. SQL Query Builder
Another classic — building SQL queries dynamically:
class QueryBuilder {
private table = "";
private conditions: string[] = [];
private columns: string[] = ["*"];
private orderColumn = "";
private orderDir: "ASC" | "DESC" = "ASC";
private limitVal?: number;
from(table: string): QueryBuilder {
this.table = table;
return this;
}
select(...columns: string[]): QueryBuilder {
this.columns = columns;
return this;
}
where(condition: string): QueryBuilder {
this.conditions.push(condition);
return this;
}
orderBy(column: string, direction: "ASC" | "DESC" = "ASC"): QueryBuilder {
this.orderColumn = column;
this.orderDir = direction;
return this;
}
limit(n: number): QueryBuilder {
this.limitVal = n;
return this;
}
build(): string {
if (!this.table) throw new Error("Table name is required");
let query = `SELECT ${this.columns.join(", ")} FROM ${this.table}`;
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(" AND ")}`;
}
if (this.orderColumn) {
query += ` ORDER BY ${this.orderColumn} ${this.orderDir}`;
}
if (this.limitVal !== undefined) {
query += ` LIMIT ${this.limitVal}`;
}
return query;
}
}
// Usage
const query = new QueryBuilder()
.from("users")
.select("id", "name", "email")
.where("age > 18")
.where("active = true")
.orderBy("name", "ASC")
.limit(10)
.build();
// SELECT id, name, email FROM users WHERE age > 18 AND active = true ORDER BY name ASC LIMIT 10
console.log(query);3. Notification Builder (Python)
from dataclasses import dataclass, field
from typing import List, Optional
from enum import Enum
class NotificationChannel(Enum):
EMAIL = "email"
SMS = "sms"
PUSH = "push"
@dataclass
class Notification:
title: str
body: str
channels: List[NotificationChannel]
recipient: str
priority: str = "normal"
scheduled_at: Optional[str] = None
metadata: dict = field(default_factory=dict)
class NotificationBuilder:
def __init__(self) -> None:
self._title: Optional[str] = None
self._body: Optional[str] = None
self._channels: List[NotificationChannel] = []
self._recipient: Optional[str] = None
self._priority = "normal"
self._scheduled_at: Optional[str] = None
self._metadata: dict = {}
def title(self, title: str) -> "NotificationBuilder":
self._title = title
return self
def body(self, body: str) -> "NotificationBuilder":
self._body = body
return self
def to(self, recipient: str) -> "NotificationBuilder":
self._recipient = recipient
return self
def via(self, *channels: NotificationChannel) -> "NotificationBuilder":
self._channels.extend(channels)
return self
def urgent(self) -> "NotificationBuilder":
self._priority = "high"
return self
def schedule(self, at: str) -> "NotificationBuilder":
self._scheduled_at = at
return self
def with_metadata(self, **kwargs) -> "NotificationBuilder":
self._metadata.update(kwargs)
return self
def build(self) -> Notification:
if not self._title:
raise ValueError("Title is required")
if not self._body:
raise ValueError("Body is required")
if not self._recipient:
raise ValueError("Recipient is required")
if not self._channels:
raise ValueError("At least one channel is required")
return Notification(
title=self._title,
body=self._body,
channels=self._channels,
recipient=self._recipient,
priority=self._priority,
scheduled_at=self._scheduled_at,
metadata=self._metadata,
)
# Usage - reads like English
notification = (
NotificationBuilder()
.title("New Order")
.body("Your order #1234 has shipped!")
.to("user@example.com")
.via(NotificationChannel.EMAIL, NotificationChannel.PUSH)
.urgent()
.with_metadata(order_id="1234", tracking="XYZ789")
.build()
)Builder vs Other Creational Patterns
| Pattern | When to Use | Creates |
|---|---|---|
| Builder | Complex object with many optional parts | One complex object |
| Factory Method | Subclasses decide which class to instantiate | Family of related objects |
| Abstract Factory | Families of related objects without specifying classes | Multiple related objects |
| Prototype | Copy an existing object | Clone of an object |
| Singleton | Exactly one instance is needed | Single shared instance |
Use Builder when:
- Object has 4+ constructor parameters (especially optional ones)
- Object construction requires multiple steps
- You want immutable objects with validation
- You want a readable, self-documenting API
Don't use Builder when:
- Object has only 1–3 simple required fields (overkill)
- Object is mutable anyway (no immutability benefit)
- A simple factory function suffices
Common Mistakes
1. Mutable Product
// ❌ Builder creates mutable object - defeats the purpose
class UserBuilder {
build(): User {
const user = new User();
user.name = this._name; // Public setter - anyone can modify
user.email = this._email;
return user;
}
}
// ✅ Builder creates immutable object
class UserBuilder {
build(): User {
return new User(this._name, this._email, ...); // Private constructor
}
}2. Forgetting to Return this
// ❌ Breaks chaining
class UserBuilder {
setName(name: string): void { // Returns void!
this._name = name;
// forgot return this
}
}
// ✅ Always return this from setter methods
class UserBuilder {
setName(name: string): UserBuilder {
this._name = name;
return this;
}
}3. Skipping Validation in build()
// ❌ No validation - silently creates invalid objects
class UserBuilder {
build(): User {
return new User(this._name!, this._email!); // ! bypasses null check
}
}
// ✅ Validate in build()
class UserBuilder {
build(): User {
if (!this._name) throw new Error("Name is required");
if (!this._email || !this._email.includes("@")) {
throw new Error("Valid email is required");
}
return new User(this._name, this._email);
}
}4. Builder Without Reset (Reuse Bug)
// ❌ Reusing builder creates contaminated objects
const builder = new UserBuilder().setName("Alice").setEmail("alice@example.com");
const alice = builder.build();
builder.setName("Bob"); // Forgot to set email!
const bob = builder.build(); // Bob has Alice's email!
// ✅ Either create a new builder each time, or add reset()
class UserBuilder {
reset(): UserBuilder {
this._name = undefined;
this._email = undefined;
this._age = undefined;
return this;
}
}Builder in Frameworks You Already Use
You interact with the Builder pattern constantly:
Java Streams / StringBuilder:
String result = new StringBuilder()
.append("Hello")
.append(", ")
.append("World")
.toString();Java HttpClient (Java 11+):
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10))
.build();Spring Boot MockMvc (testing):
mockMvc.perform(
MockMvcRequestBuilders.post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(json)
)Python requests session (builder-like):
session = requests.Session()
session.headers.update({"Authorization": "Bearer token"})
session.timeout = 30
response = session.get("https://api.example.com/data")TypeScript Prisma ORM:
const users = await prisma.user.findMany({
where: { active: true, age: { gte: 18 } },
select: { id: true, name: true, email: true },
orderBy: { name: "asc" },
take: 10,
});These are all Builder pattern variations under different names.
Summary
The Builder Pattern is one of the most practical creational patterns — you'll use it constantly in real-world code.
Key takeaways:
✅ Builder separates construction from representation
✅ Fluent interface (method chaining with return this) makes construction readable
✅ Validation belongs in build(), not in individual setters
✅ Private constructors enforce that only the builder can create the product
✅ Director is optional — use when you have common construction recipes
✅ Create a new builder instance per object (or add reset())
When to reach for Builder:
- 4+ constructor parameters (especially optional ones)
- Object needs step-by-step construction with validation
- You want an immutable, self-documenting API
What's Next
You've completed Phase 3: Creational Patterns:
- ✅ Singleton Pattern
- ✅ Factory and Abstract Factory Patterns
- ✅ Builder Pattern (this post)
- ➡️ Prototype Pattern — clone objects efficiently
Then we move to Phase 4: Structural Patterns:
Part 10 of the OOP & Design Patterns series. Building on Factory and Abstract Factory — the previous creational pattern.
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.