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.
Builder Pattern
Why This Matters
Think of the Builder pattern like ordering a custom pizza. Instead of a constructor with 20 parameters (size, crust, cheese, pepperoni, mushrooms, etc.), you use a builder: Pizza.builder().size("large").crust("thin").cheese().pepperoni().build(). The Builder pattern does the same for code—it lets you construct complex objects step by step, making code more readable and flexible.
This matters because some objects have many optional parameters. A constructor with 10 parameters is hard to read and error-prone (which parameter is which?). The Builder pattern lets you set only the parameters you need, in any order, making code more readable and maintainable.
In interviews, when someone asks "How would you create an object with many optional parameters?", they're testing whether you understand the Builder pattern. Do you know how to make construction readable? Can you handle optional parameters elegantly? Most engineers can't. They use constructors with many parameters or setters, making code hard to read.
What Engineers Usually Get Wrong
Engineers often think "Builder pattern is just for complex objects." But Builder pattern is useful whenever you have many optional parameters, even for simple objects. It makes code more readable than constructors with many parameters or chains of setters. Use it when readability matters.
Engineers also don't understand that Builder pattern adds complexity. For simple objects with few parameters, a constructor is fine. Use Builder when you have many optional parameters or when you need to validate parameters before construction. Don't over-engineer—simple objects don't need builders.
How This Breaks Systems in the Real World
A service was creating configuration objects with 20 parameters. The constructor had 20 parameters, making it hard to read and error-prone. Developers often passed parameters in the wrong order, causing bugs. The fix? Use Builder pattern. Create a builder that sets parameters step by step. Now code is readable: Config.builder().host("example.com").port(8080).timeout(30).build(). This reduces errors and improves maintainability.
Another story: A service was using setters to configure objects. Code had 20 setter calls, making it hard to see what was being configured. Also, objects could be used before all required parameters were set, causing runtime errors. The fix? Use Builder pattern. Builders can validate that all required parameters are set before construction. This prevents runtime errors and makes configuration clear.
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
1// Product2class Pizza {3 private dough: string = "";4 private sauce: string = "";5 private toppings: string[] = [];67 setDough(dough: string): void {8 this.dough = dough;9 }1011 setSauce(sauce: string): void {12 this.sauce = sauce;13 }1415 addTopping(topping:
Fluent Builder (Method Chaining)
1class User {2 private name: string = "";3 private email: string = "";4 private age: number = 0;5 private address: string = "";67 constructor(private builder: UserBuilder) {8 this.name = builder.name;9 this.email = builder.email;10 this.age = builder.age;11 this.address = builder.address;12 }13}
SQL Query Builder
1class QueryBuilder {2 private selectFields: string[] = [];3 private fromTable: string = "";4 private whereConditions: string[] = [];5 private orderBy: string = "";6 private limitCount: number = 0;78 select(fields: string[]): QueryBuilder {9 this.selectFields = fields;10 return this;11 }
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:
1const user = new UserBuilder()2 .setName("John")3 .setEmail("john@example.com")4 .setAge(30)5 .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:
1// Product2class Pizza { /* ... */ }34// Builder5interface PizzaBuilder {6 buildDough(): void;7 buildSauce(): void;8 buildToppings(): void;9 getPizza(): Pizza;10}1112// ConcreteBuilder13class MargheritaBuilder implements PizzaBuilder {14 private pizza: Pizza = new Pizza();1516 buildDough():
Fluent Builder (without Director):
1class UserBuilder {2 setName(name: string): UserBuilder { /* ... */ }3 setEmail(email: string): UserBuilder { /* ... */ }4 build(): User { /* ... */ }5}
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:
1// Configuration product2class Configuration {3 private settings: Map<string, any> = new Map();45 set(key: string, value: any): void {6 this.settings.set(key, value);7 }89 get(key: string): any {10 return this.settings.get(key);11 }1213 toJSON
Features:
- Fluent interface: Method chaining
- Validation: Validate before building
- Optional parameters: Only set what's needed
- Multiple formats: Export to different formats
-
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
-
Factory Pattern - Both patterns handle object creation. Factories choose which class, builders construct complex objects. Understanding both helps choose the right pattern.
-
SOLID Principles - Builder pattern follows Single Responsibility Principle (construction separate from representation). Understanding SOLID helps understand builder benefits.
-
DRY, KISS & YAGNI - Builders can add complexity. Understanding DRY/KISS/YAGNI helps decide when builders are worth it.
-
Separation of Concerns - Builders separate construction from representation. Understanding separation of concerns helps understand builder benefits.
-
Command Pattern - Both patterns encapsulate operations. Builders encapsulate construction, Commands encapsulate requests.
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
Related Topics
Factory Pattern
Both patterns handle object creation. Factories choose which class, builders construct complex objects. Understanding both helps choose the right pattern.
SOLID Principles
Builder pattern follows Single Responsibility Principle (construction separate from representation). Understanding SOLID helps understand builder benefits.
DRY, KISS & YAGNI
Builders can add complexity. Understanding DRY/KISS/YAGNI helps decide when builders are worth it.
Separation of Concerns
Builders separate construction from representation. Understanding separation of concerns helps understand builder benefits.
Command Pattern
Both patterns encapsulate operations. Builders encapsulate construction, Commands encapsulate requests.
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.