Design Principle
Builder Pattern
Learn the Builder pattern: construct complex objects step by step. Separate construction from representation and create objects with many optional parameters.
The Builder Pattern constructs complex objects step by step. It allows you to produce different types and representations of an object using the same construction code.
What is the Builder Pattern?
Builder Pattern provides:
- Step-by-step construction: Build objects incrementally
- Flexible construction: Same construction process, different representations
- Optional parameters: Handle many optional parameters elegantly
- Readability: More readable than constructors with many parameters
Use cases:
- Complex objects with many optional parameters
- Objects that require step-by-step construction
- Creating different representations of the same object
- SQL query builders
- HTTP request builders
Structure
Director
└─ construct() (uses builder)
Builder (interface)
├─ buildPartA()
├─ buildPartB()
└─ getResult()
ConcreteBuilder
└─ implements Builder
Product
Examples
Basic Builder Pattern
// Product
class Pizza {
private dough: string = "";
private sauce: string = "";
private toppings: string[] = [];
setDough(dough: string): void {
this.dough = dough;
}
setSauce(sauce: string): void {
this.sauce = sauce;
}
addTopping(topping: string): void {
this.toppings.push(topping);
}
describe(): string {
return `Pizza with ${this.dough} dough, ${this.sauce} sauce, and toppings: ${this.toppings.join(", ")}`;
}
}
// Builder interface
interface PizzaBuilder {
buildDough(): void;
buildSauce(): void;
buildToppings(): void;
getPizza(): Pizza;
}
// Concrete builder
class MargheritaPizzaBuilder implements PizzaBuilder {
private pizza: Pizza = new Pizza();
buildDough(): void {
this.pizza.setDough("thin crust");
}
buildSauce(): void {
this.pizza.setSauce("tomato");
}
buildToppings(): void {
this.pizza.addTopping("mozzarella");
this.pizza.addTopping("basil");
}
getPizza(): Pizza {
return this.pizza;
}
}
// Director
class PizzaDirector {
construct(builder: PizzaBuilder): Pizza {
builder.buildDough();
builder.buildSauce();
builder.buildToppings();
return builder.getPizza();
}
}
// Usage
const director = new PizzaDirector();
const builder = new MargheritaPizzaBuilder();
const pizza = director.construct(builder);
console.log(pizza.describe());
Fluent Builder (Method Chaining)
class User {
private name: string = "";
private email: string = "";
private age: number = 0;
private address: string = "";
constructor(private builder: UserBuilder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.address = builder.address;
}
}
class UserBuilder {
name: string = "";
email: string = "";
age: number = 0;
address: string = "";
setName(name: string): UserBuilder {
this.name = name;
return this;
}
setEmail(email: string): UserBuilder {
this.email = email;
return this;
}
setAge(age: number): UserBuilder {
this.age = age;
return this;
}
setAddress(address: string): UserBuilder {
this.address = address;
return this;
}
build(): User {
return new User(this);
}
}
// Usage
const user = new UserBuilder()
.setName("John Doe")
.setEmail("john@example.com")
.setAge(30)
.setAddress("123 Main St")
.build();
SQL Query Builder
class QueryBuilder {
private selectFields: string[] = [];
private fromTable: string = "";
private whereConditions: string[] = [];
private orderBy: string = "";
private limitCount: number = 0;
select(fields: string[]): QueryBuilder {
this.selectFields = fields;
return this;
}
from(table: string): QueryBuilder {
this.fromTable = table;
return this;
}
where(condition: string): QueryBuilder {
this.whereConditions.push(condition);
return this;
}
orderBy(field: string): QueryBuilder {
this.orderBy = field;
return this;
}
limit(count: number): QueryBuilder {
this.limitCount = count;
return this;
}
build(): string {
let query = `SELECT ${this.selectFields.join(", ")} FROM ${this.fromTable}`;
if (this.whereConditions.length > 0) {
query += ` WHERE ${this.whereConditions.join(" AND ")}`;
}
if (this.orderBy) {
query += ` ORDER BY ${this.orderBy}`;
}
if (this.limitCount > 0) {
query += ` LIMIT ${this.limitCount}`;
}
return query;
}
}
// Usage
const query = new QueryBuilder()
.select(["id", "name", "email"])
.from("users")
.where("age > 18")
.where("status = 'active'")
.orderBy("name")
.limit(10)
.build();
// SELECT id, name, email FROM users WHERE age > 18 AND status = 'active' ORDER BY name LIMIT 10
Common Pitfalls
- Over-engineering: Using builder for simple objects. Fix: Use builder only for complex objects
- Missing validation: Not validating required fields. Fix: Validate in build() method
- Immutable objects: Builder creates mutable objects. Fix: Make product immutable
- Director not needed: Director adds unnecessary complexity. Fix: Use fluent builder without director
Interview Questions
Beginner
Q: What is the Builder pattern and when would you use it?
A:
Builder Pattern constructs complex objects step by step.
Benefits:
- Step-by-step construction: Build objects incrementally
- Optional parameters: Handle many optional parameters
- Readability: More readable than constructors with many parameters
- Flexibility: Same construction process, different representations
Example:
const user = new UserBuilder()
.setName("John")
.setEmail("john@example.com")
.setAge(30)
.build();
Use cases:
- Complex objects: Objects with many optional parameters
- Step-by-step construction: Objects that require incremental building
- Different representations: Same process, different results
- Query builders: SQL, HTTP request builders
Alternative to:
- Telescoping constructors: Multiple constructors with different parameters
- JavaBeans pattern: Setter methods (not thread-safe)
Intermediate
Q: Explain the Builder pattern structure. What is the role of Director, Builder, and Product?
A:
Builder Pattern Structure:
1. Product:
The complex object being built
2. Builder (Interface):
Defines steps to build product
- buildPartA()
- buildPartB()
- getResult()
3. ConcreteBuilder:
Implements builder interface
Builds specific representation
4. Director (Optional):
Uses builder to construct product
Defines construction order
Example:
// Product
class Pizza { /* ... */ }
// Builder
interface PizzaBuilder {
buildDough(): void;
buildSauce(): void;
buildToppings(): void;
getPizza(): Pizza;
}
// ConcreteBuilder
class MargheritaBuilder implements PizzaBuilder {
private pizza: Pizza = new Pizza();
buildDough(): void {
this.pizza.setDough("thin");
}
// ... other methods
getPizza(): Pizza {
return this.pizza;
}
}
// Director
class Director {
construct(builder: PizzaBuilder): Pizza {
builder.buildDough();
builder.buildSauce();
builder.buildToppings();
return builder.getPizza();
}
}
Fluent Builder (without Director):
class UserBuilder {
setName(name: string): UserBuilder { /* ... */ }
setEmail(email: string): UserBuilder { /* ... */ }
build(): User { /* ... */ }
}
Senior
Q: Design a builder system for constructing complex configuration objects with validation, optional parameters, and support for different configuration formats (JSON, YAML, XML).
A:
// Configuration product
class Configuration {
private settings: Map<string, any> = new Map();
set(key: string, value: any): void {
this.settings.set(key, value);
}
get(key: string): any {
return this.settings.get(key);
}
toJSON(): string {
return JSON.stringify(Object.fromEntries(this.settings));
}
toYAML(): string {
// Convert to YAML
return this.convertToYAML();
}
toXML(): string {
// Convert to XML
return this.convertToXML();
}
}
// Builder interface
interface ConfigurationBuilder {
setDatabase(config: DatabaseConfig): ConfigurationBuilder;
setServer(config: ServerConfig): ConfigurationBuilder;
setCache(config: CacheConfig): ConfigurationBuilder;
setLogging(config: LoggingConfig): ConfigurationBuilder;
validate(): void;
build(): Configuration;
}
// Concrete builder
class ConfigurationBuilderImpl implements ConfigurationBuilder {
private config: Configuration = new Configuration();
private validators: Validator[] = [];
setDatabase(config: DatabaseConfig): ConfigurationBuilder {
this.config.set("database", config);
return this;
}
setServer(config: ServerConfig): ConfigurationBuilder {
this.config.set("server", config);
return this;
}
setCache(config: CacheConfig): ConfigurationBuilder {
this.config.set("cache", config);
return this;
}
setLogging(config: LoggingConfig): ConfigurationBuilder {
this.config.set("logging", config);
return this;
}
validate(): void {
// Validate required fields
if (!this.config.get("database")) {
throw new Error("Database configuration required");
}
// Run custom validators
for (const validator of this.validators) {
validator.validate(this.config);
}
}
build(): Configuration {
this.validate();
return this.config;
}
addValidator(validator: Validator): ConfigurationBuilder {
this.validators.push(validator);
return this;
}
}
// Usage
const config = new ConfigurationBuilderImpl()
.setDatabase({ host: "localhost", port: 5432 })
.setServer({ port: 3000, host: "0.0.0.0" })
.setCache({ type: "redis", ttl: 3600 })
.addValidator(new DatabaseValidator())
.build();
console.log(config.toJSON());
Features:
- Fluent interface: Method chaining
- Validation: Validate before building
- Optional parameters: Only set what's needed
- Multiple formats: Export to different formats
Key Takeaways
- Builder pattern: Constructs complex objects step by step
- Benefits: Handles optional parameters, improves readability, flexible construction
- Structure: Product, Builder, ConcreteBuilder, Director (optional)
- Fluent builder: Method chaining for better readability
- Use cases: Complex objects, query builders, configuration objects
- Best practices: Validate in build(), make product immutable, avoid over-engineering
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.