← Back to principles

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:

  1. Fluent interface: Method chaining
  2. Validation: Validate before building
  3. Optional parameters: Only set what's needed
  4. 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.