Skip to content

Builder Pattern

The Builder pattern separates the construction of a complex object from its representation. Itโ€™s especially useful when an object has many optional parameters or requires a specific construction sequence.

A class with many optional parameters leads to:

// Telescoping constructor anti-pattern
var email = new Email("to@example.com", "From Name", "Subject", "Body",
null, null, "reply@example.com", true, false, Priority.High, null);
// What does that 8th boolean mean?
public class Email
{
public string To { get; }
public string Subject { get; }
public string Body { get; }
public string? From { get; }
public string? ReplyTo { get; }
public bool IsHtml { get; }
public Priority Priority { get; }
public IReadOnlyList<string> Cc { get; }
private Email(Builder builder)
{
To = builder.To;
Subject = builder.Subject;
Body = builder.Body;
From = builder.From;
ReplyTo = builder.ReplyTo;
IsHtml = builder.IsHtml;
Priority = builder.Priority;
Cc = builder.Cc.AsReadOnly();
}
public class Builder
{
public string To { get; }
public string Subject { get; }
public string Body { get; }
public string? From { get; private set; }
public string? ReplyTo { get; private set; }
public bool IsHtml { get; private set; }
public Priority Priority { get; private set; } = Priority.Normal;
public List<string> Cc { get; } = new();
public Builder(string to, string subject, string body)
{
To = to;
Subject = subject;
Body = body;
}
public Builder WithFrom(string from) { From = from; return this; }
public Builder WithReplyTo(string replyTo) { ReplyTo = replyTo; return this; }
public Builder AsHtml() { IsHtml = true; return this; }
public Builder WithPriority(Priority p) { Priority = p; return this; }
public Builder WithCc(string address) { Cc.Add(address); return this; }
public Email Build() => new(this);
}
}
// Usage โ€” only set what you need
var email = new Email.Builder("alice@example.com", "Order Confirmed", body)
.AsHtml()
.WithPriority(Priority.High)
.WithReplyTo("support@example.com")
.Build();
class QueryBuilder {
private table = '';
private conditions: string[] = [];
private columns: string[] = ['*'];
private orderByClause = '';
private limitValue?: number;
from(table: string): this {
this.table = table;
return this;
}
select(...columns: string[]): this {
this.columns = columns;
return this;
}
where(condition: string): this {
this.conditions.push(condition);
return this;
}
orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
this.orderByClause = `ORDER BY ${column} ${direction}`;
return this;
}
limit(n: number): this {
this.limitValue = n;
return this;
}
build(): string {
if (!this.table) throw new Error('Table is required');
let sql = `SELECT ${this.columns.join(', ')} FROM ${this.table}`;
if (this.conditions.length > 0) {
sql += ` WHERE ${this.conditions.join(' AND ')}`;
}
if (this.orderByClause) sql += ` ${this.orderByClause}`;
if (this.limitValue) sql += ` LIMIT ${this.limitValue}`;
return sql;
}
}
// Readable and flexible
const query = new QueryBuilder()
.from('users')
.select('id', 'name', 'email')
.where('active = true')
.where('created_at > NOW() - INTERVAL 30 DAYS')
.orderBy('created_at', 'DESC')
.limit(20)
.build();
class UserBuilder {
private name = '';
private email = '';
private age?: number;
withName(name: string): this { this.name = name; return this; }
withEmail(email: string): this { this.email = email; return this; }
withAge(age: number): this { this.age = age; return this; }
build(): User {
if (!this.name) throw new Error('Name is required');
if (!this.email) throw new Error('Email is required');
if (!this.email.includes('@')) throw new Error('Email is invalid');
if (this.age !== undefined && this.age < 0) throw new Error('Age must be positive');
return new User(this.name, this.email, this.age);
}
}

Builders are especially valuable in test setup โ€” use the โ€œTest Data Builderโ€ or โ€œObject Motherโ€ pattern:

class UserBuilder {
private data = {
id: '1',
name: 'Test User',
email: 'test@example.com',
role: 'user' as UserRole,
createdAt: new Date(),
};
withId(id: string) { this.data.id = id; return this; }
withName(name: string) { this.data.name = name; return this; }
withEmail(email: string) { this.data.email = email; return this; }
asAdmin() { this.data.role = 'admin'; return this; }
build(): User { return new User(this.data); }
}
// Clean, readable test setup
const admin = new UserBuilder().withName('Alice').asAdmin().build();
const user = new UserBuilder().withEmail('bob@example.com').build();
  • Object has more than 3-4 optional parameters
  • Object construction requires multiple steps or validation
  • You want readable, self-documenting object creation
  • Creating test fixtures