Skip to content

Builder Pattern

Build complex objects step by step - construct with clarity and flexibility!

Builder Pattern: Constructing Complex Objects Step by Step

Section titled “Builder Pattern: Constructing Complex Objects Step by Step”

Now let’s dive into the Builder Pattern - a creational design pattern that helps you construct complex objects step by step.

Imagine ordering a custom pizza. You don’t just say “I want a pizza” - you specify the size, crust type, sauce, cheese, and toppings one by one. The Builder Pattern works the same way!

The Builder Pattern lets you construct complex objects step by step. Instead of passing many parameters to a constructor (which gets messy), you use a builder to set properties one at a time.

The Builder Pattern is useful when:

  1. Objects have many optional parameters - Constructor with 10+ parameters is messy
  2. Complex object construction - Objects need step-by-step setup with validation
  3. You want readable code - Method chaining makes code self-documenting
  4. You need different representations - Same construction process, different results
  5. You want to avoid telescoping constructors - Multiple constructors with different parameters

What Happens If We Don’t Use Builder Pattern?

Section titled “What Happens If We Don’t Use Builder Pattern?”

Without the Builder Pattern, you might:

  • Create telescoping constructors - Multiple constructors with different parameters
  • Use setters everywhere - Objects in incomplete state until all setters called
  • Pass many parameters - Constructor with 10+ parameters is hard to read and error-prone
  • Lose immutability - Objects can be modified after creation
  • Violate Single Responsibility - Object handles both construction and business logic

Let’s start with a super simple example that anyone can understand!

Diagram

Here’s how the Builder Pattern works in practice - showing the step-by-step construction:

sequenceDiagram
    participant Client
    participant Builder as PizzaBuilder
    participant Product as Pizza
    
    Client->>Builder: new PizzaBuilder()
    activate Builder
    Builder->>Product: new Pizza()
    activate Product
    deactivate Product
    
    Client->>Builder: with_size("large")
    Builder->>Product: set size
    Client->>Builder: with_crust("thin")
    Builder->>Product: set crust
    Client->>Builder: with_sauce("tomato")
    Builder->>Product: set sauce
    Client->>Builder: add_topping("pepperoni")
    Builder->>Product: add topping
    
    Client->>Builder: build()
    Builder->>Builder: validate()
    Builder-->>Client: Returns Pizza instance
    deactivate Builder
    
    Note over Client,Product: Builder constructs Pizza<br/>step by step with validation!

You’re building a pizza ordering system where customers can customize their pizza with size, crust, sauce, cheese, and toppings. Without Builder Pattern:

bad_pizza.py
# ❌ Without Builder Pattern - Messy constructors!
class Pizza:
def __init__(self, size=None, crust=None, sauce=None, cheese=None,
pepperoni=False, mushrooms=False, onions=False, peppers=False):
self.size = size
self.crust = crust
self.sauce = sauce
self.cheese = cheese
self.pepperoni = pepperoni
self.mushrooms = mushrooms
self.onions = onions
self.peppers = peppers
def describe(self):
toppings = []
if self.pepperoni:
toppings.append("pepperoni")
if self.mushrooms:
toppings.append("mushrooms")
if self.onions:
toppings.append("onions")
if self.peppers:
toppings.append("peppers")
return f"{self.size} {self.crust} pizza with {self.sauce} sauce, {self.cheese} cheese, and {', '.join(toppings) if toppings else 'no'} toppings"
# Problem: Hard to read and error-prone!
pizza1 = Pizza("large", "thin", "tomato", "mozzarella", True, False, True, False)
pizza2 = Pizza(size="medium", crust="thick", sauce="tomato", cheese="cheddar",
pepperoni=False, mushrooms=True, onions=False, peppers=True)
# Problems:
# - Hard to remember parameter order
# - Easy to mix up parameters
# - What if I only want size and crust? Need to pass None for everything else
# - Can't validate until object is created

Problems:

  • Hard to read - What does True, False, True, False mean?
  • Easy to make mistakes - Wrong parameter order
  • Inflexible - Need to pass all parameters even if you don’t need them
  • No validation - Can create invalid pizzas
  • Not self-documenting - Code doesn’t explain what it’s doing
classDiagram
    class Pizza {
        -size: str
        -crust: str
        -sauce: str
        -cheese: str
        -toppings: List
        +describe() str
    }
    class PizzaBuilder {
        -pizza: Pizza
        +with_size(size) PizzaBuilder
        +with_crust(crust) PizzaBuilder
        +with_sauce(sauce) PizzaBuilder
        +with_cheese(cheese) PizzaBuilder
        +add_topping(topping) PizzaBuilder
        +build() Pizza
    }
    class Client {
        +create_pizza() Pizza
    }
    
    PizzaBuilder --> Pizza : constructs
    Client --> PizzaBuilder : uses
    Client ..> Pizza : uses
    
    note for PizzaBuilder "Returns self for<br/>method chaining"
    note for PizzaBuilder "Validates before<br/>returning Pizza"
pizza_builder.py
from typing import List
# Step 1: Create the Product class
class Pizza:
def __init__(self):
self.size = None
self.crust = None
self.sauce = None
self.cheese = None
self.toppings: List[str] = []
def describe(self) -> str:
toppings_str = ', '.join(self.toppings) if self.toppings else 'no'
return f"{self.size} {self.crust} pizza with {self.sauce} sauce, {self.cheese} cheese, and {toppings_str} toppings"
# Step 2: Create the Builder
class PizzaBuilder:
"""Builder for constructing Pizza objects step by step"""
def __init__(self):
self.pizza = Pizza()
def with_size(self, size: str) -> 'PizzaBuilder':
"""Set pizza size"""
self.pizza.size = size
return self # Return self for method chaining
def with_crust(self, crust: str) -> 'PizzaBuilder':
"""Set crust type"""
self.pizza.crust = crust
return self
def with_sauce(self, sauce: str) -> 'PizzaBuilder':
"""Set sauce type"""
self.pizza.sauce = sauce
return self
def with_cheese(self, cheese: str) -> 'PizzaBuilder':
"""Set cheese type"""
self.pizza.cheese = cheese
return self
def add_topping(self, topping: str) -> 'PizzaBuilder':
"""Add a topping"""
self.pizza.toppings.append(topping)
return self
def build(self) -> Pizza:
"""Build and return the pizza"""
# Validate before building
if not self.pizza.size:
raise ValueError("Pizza size is required")
if not self.pizza.crust:
raise ValueError("Pizza crust is required")
if not self.pizza.sauce:
raise ValueError("Pizza sauce is required")
if not self.pizza.cheese:
raise ValueError("Pizza cheese is required")
return self.pizza
# Step 3: Use the builder - clean and readable!
def create_pizza():
# Method chaining makes it self-documenting!
pizza1 = (PizzaBuilder()
.with_size("large")
.with_crust("thin")
.with_sauce("tomato")
.with_cheese("mozzarella")
.add_topping("pepperoni")
.add_topping("onions")
.build())
pizza2 = (PizzaBuilder()
.with_size("medium")
.with_crust("thick")
.with_sauce("tomato")
.with_cheese("cheddar")
.add_topping("mushrooms")
.add_topping("peppers")
.build())
print(pizza1.describe())
print(pizza2.describe())
# Benefits:
# - Self-documenting code
# - Easy to add/remove options
# - Validation before building
# - Flexible - only set what you need

Real-World Software Example: Building HTTP Requests

Section titled “Real-World Software Example: Building HTTP Requests”

Now let’s see a realistic software example - building HTTP requests with many optional parameters (headers, query params, body, etc.).

You’re building an HTTP client library. HTTP requests have many optional components: headers, query parameters, request body, authentication, timeout, retries, etc. Without Builder Pattern:

bad_http.py
# ❌ Without Builder Pattern - Messy!
class HttpRequest:
def __init__(self, method, url, headers=None, params=None, body=None,
auth=None, timeout=None, retries=None, follow_redirects=None):
self.method = method
self.url = url
self.headers = headers or {}
self.params = params or {}
self.body = body
self.auth = auth
self.timeout = timeout
self.retries = retries
self.follow_redirects = follow_redirects
def execute(self):
# Build and execute HTTP request
print(f"Executing {self.method} {self.url}")
if self.headers:
print(f"Headers: {self.headers}")
if self.params:
print(f"Params: {self.params}")
if self.body:
print(f"Body: {self.body}")
return {"status": "success"}
# Problem: Hard to read and error-prone!
request1 = HttpRequest(
"GET",
"https://api.example.com/users",
headers={"Authorization": "Bearer token123"},
params={"page": "1", "limit": "10"},
timeout=30,
retries=3,
follow_redirects=True
)
# What if I only need method and URL? Still need to pass None for everything!
request2 = HttpRequest("GET", "https://api.example.com/ping", None, None, None, None, None, None, None)
# Problems:
# - Hard to read - what do all those None values mean?
# - Easy to mix up parameter order
# - Can't validate until execution
# - Not self-documenting

Problems:

  • Hard to read - Many None/null values
  • Error-prone - Easy to mix up parameter order
  • Inflexible - Must pass all parameters even if unused
  • No validation - Can create invalid requests
  • Not self-documenting - Code doesn’t explain itself
http_builder.py
from typing import Dict, Optional
# Step 1: Create the Product class
class HttpRequest:
def __init__(self):
self.method: Optional[str] = None
self.url: Optional[str] = None
self.headers: Dict[str, str] = {}
self.params: Dict[str, str] = {}
self.body: Optional[str] = None
self.auth: Optional[str] = None
self.timeout: Optional[int] = None
self.retries: Optional[int] = None
self.follow_redirects: Optional[bool] = None
def execute(self) -> Dict[str, str]:
"""Execute the HTTP request"""
print(f"Executing {self.method} {self.url}")
if self.headers:
print(f"Headers: {self.headers}")
if self.params:
print(f"Params: {self.params}")
if self.body:
print(f"Body: {self.body}")
return {"status": "success"}
# Step 2: Create the Builder
class HttpRequestBuilder:
"""Builder for constructing HttpRequest objects step by step"""
def __init__(self):
self.request = HttpRequest()
def with_method(self, method: str) -> 'HttpRequestBuilder':
"""Set HTTP method"""
self.request.method = method.upper()
return self
def with_url(self, url: str) -> 'HttpRequestBuilder':
"""Set request URL"""
self.request.url = url
return self
def add_header(self, key: str, value: str) -> 'HttpRequestBuilder':
"""Add a header"""
self.request.headers[key] = value
return self
def add_param(self, key: str, value: str) -> 'HttpRequestBuilder':
"""Add a query parameter"""
self.request.params[key] = value
return self
def with_body(self, body: str) -> 'HttpRequestBuilder':
"""Set request body"""
self.request.body = body
return self
def with_auth(self, token: str) -> 'HttpRequestBuilder':
"""Set authentication token"""
self.request.auth = token
self.request.headers["Authorization"] = f"Bearer {token}"
return self
def with_timeout(self, seconds: int) -> 'HttpRequestBuilder':
"""Set request timeout"""
self.request.timeout = seconds
return self
def with_retries(self, count: int) -> 'HttpRequestBuilder':
"""Set number of retries"""
self.request.retries = count
return self
def follow_redirects(self, follow: bool = True) -> 'HttpRequestBuilder':
"""Set whether to follow redirects"""
self.request.follow_redirects = follow
return self
def build(self) -> HttpRequest:
"""Build and return the HTTP request"""
# Validate before building
if not self.request.method:
raise ValueError("HTTP method is required")
if not self.request.url:
raise ValueError("URL is required")
return self.request
# Step 3: Use the builder - clean and readable!
def make_requests():
# Complex request - self-documenting!
request1 = (HttpRequestBuilder()
.with_method("GET")
.with_url("https://api.example.com/users")
.add_header("Authorization", "Bearer token123")
.add_header("Content-Type", "application/json")
.add_param("page", "1")
.add_param("limit", "10")
.with_timeout(30)
.with_retries(3)
.follow_redirects(True)
.build())
# Simple request - only what you need!
request2 = (HttpRequestBuilder()
.with_method("GET")
.with_url("https://api.example.com/ping")
.build())
# POST request with body
request3 = (HttpRequestBuilder()
.with_method("POST")
.with_url("https://api.example.com/users")
.with_auth("token456")
.with_body('{"name": "John", "email": "john@example.com"}')
.with_timeout(60)
.build())
request1.execute()
request2.execute()
request3.execute()
# Benefits:
# - Self-documenting - reads like English
# - Flexible - only set what you need
# - Validation before building
# - Easy to extend with new options

With Builder Pattern, adding a new option is super easy:

adding_new_option.py
# Step 1: Add field to HttpRequest
class HttpRequest:
def __init__(self):
# ... existing fields ...
self.verify_ssl: Optional[bool] = None # New field
# Step 2: Add method to Builder (only change needed!)
class HttpRequestBuilder:
# ... existing methods ...
def verify_ssl(self, verify: bool = True) -> 'HttpRequestBuilder':
"""Set SSL verification"""
self.request.verify_ssl = verify
return self
# Usage - automatically works!
request = (HttpRequestBuilder()
.with_method("GET")
.with_url("https://api.example.com/data")
.verify_ssl(False) # New option!
.build())
# That's it! No changes needed to existing code!

There are several variations of the Builder Pattern:

The classic builder with method chaining:

standard_builder.py
class PizzaBuilder:
def with_size(self, size: str) -> 'PizzaBuilder':
self.pizza.size = size
return self # Method chaining
def build(self) -> Pizza:
return self.pizza
# Usage
pizza = PizzaBuilder().with_size("large").with_crust("thin").build()

Builder that validates at each step:

fluent_builder.py
class PizzaBuilder:
def with_size(self, size: str) -> 'PizzaBuilder':
if size not in ["small", "medium", "large"]:
raise ValueError(f"Invalid size: {size}")
self.pizza.size = size
return self
def build(self) -> Pizza:
# Final validation
if not self.pizza.size:
raise ValueError("Size is required")
return self.pizza

A director that uses the builder to create predefined configurations:

director_builder.py
class PizzaDirector:
"""Director that creates predefined pizza configurations"""
def __init__(self, builder: PizzaBuilder):
self.builder = builder
def build_margherita(self) -> Pizza:
"""Build a standard Margherita pizza"""
return (self.builder
.with_size("large")
.with_crust("thin")
.with_sauce("tomato")
.with_cheese("mozzarella")
.build())
def build_pepperoni(self) -> Pizza:
"""Build a standard Pepperoni pizza"""
return (self.builder
.with_size("large")
.with_crust("thick")
.with_sauce("tomato")
.with_cheese("mozzarella")
.add_topping("pepperoni")
.build())
# Usage
builder = PizzaBuilder()
director = PizzaDirector(builder)
margherita = director.build_margherita()

Use Builder Pattern when:

Objects have many optional parameters - 5+ optional parameters
Complex object construction - Objects need step-by-step setup
You want readable code - Method chaining is self-documenting
You need validation - Validate before object creation
You want immutability - Build once, use forever
You’re avoiding telescoping constructors - Multiple constructors are messy

Don’t use Builder Pattern when:

Simple objects - If object has 1-2 parameters, direct construction is fine
All parameters required - If all parameters are mandatory, constructor is simpler
Performance is critical - Builder adds small overhead (usually negligible)
Over-engineering - Don’t use for simple cases


overcomplicated.py
# ❌ Don't do this for simple cases!
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
class PointBuilder:
def with_x(self, x: float):
self.point.x = x
return self
def with_y(self, y: float):
self.point.y = y
return self
def build(self):
return self.point
# ✅ Better: Direct construction for simple cases
point = Point(10, 20) # Simple and clear!
mutable_builder.py
# ❌ Builder modifying already-built object
class BadBuilder:
def build(self) -> Pizza:
pizza = Pizza()
# ... set properties ...
return pizza
def modify_pizza(self, pizza: Pizza):
pizza.size = "large" # Modifying built object!
# ✅ Better: Builder creates new object each time
class GoodBuilder:
def build(self) -> Pizza:
pizza = Pizza()
# ... set properties ...
return pizza # New object each time
no_validation.py
# ❌ Builder without validation
class BadBuilder:
def build(self) -> Pizza:
return self.pizza # No validation!
# ✅ Better: Validate before building
class GoodBuilder:
def build(self) -> Pizza:
if not self.pizza.size:
raise ValueError("Size is required")
if not self.pizza.crust:
raise ValueError("Crust is required")
return self.pizza

  1. Readability - Code reads like English sentences
  2. Flexibility - Only set the options you need
  3. Validation - Can validate before object creation
  4. Immutability - Objects can be immutable after building
  5. Extensibility - Easy to add new options without breaking existing code
  6. Self-Documenting - Method names explain what they do

Builder Pattern is a creational design pattern that lets you construct complex objects step by step. Instead of passing many parameters to a constructor, you use a builder to set properties one at a time.

  • Readable - Code reads like English
  • Flexible - Only set what you need
  • Validated - Validate before building
  • Self-documenting - Method names explain themselves
  • Extensible - Easy to add new options
  1. Create Product class - The object you want to build
  2. Create Builder class - Has methods to set each property
  3. Method chaining - Each method returns builder for chaining
  4. Build method - Validates and returns the final object
Client → Builder.with_x().with_y().build() → Product
  • Product - The object being built
  • Builder - Constructs the product step by step
  • build() - Validates and returns the product
  • Method chaining - Each method returns builder
# Product
class Pizza:
def __init__(self):
self.size = None
self.crust = None
# Builder
class PizzaBuilder:
def with_size(self, size):
self.pizza.size = size
return self
def build(self):
return self.pizza
# Usage
pizza = PizzaBuilder().with_size("large").with_crust("thin").build()

✅ Many optional parameters
✅ Complex object construction
✅ Need validation before creation
✅ Want readable, self-documenting code
✅ Avoiding telescoping constructors

❌ Simple objects with few parameters
❌ All parameters required
❌ Performance is critical
❌ Over-engineering simple cases

  • Builder Pattern = Construct objects step by step
  • Builder = Step-by-step construction with validation
  • Method chaining = Each method returns builder
  • Benefit = Readable, flexible, validated construction
  • Principle = Build complex objects with clarity
# 1. Product
class Product:
def __init__(self):
self.field1 = None
self.field2 = None
# 2. Builder
class Builder:
def __init__(self):
self.product = Product()
def with_field1(self, value):
self.product.field1 = value
return self
def build(self):
# Validate
return self.product
# 3. Usage
product = Builder().with_field1("value").build()
  • Builder Pattern simplifies complex object construction
  • It makes code readable and self-documenting
  • It allows validation before object creation
  • Use it when construction is complex or you have many optional parameters
  • Don’t use it for simple cases - avoid over-engineering!

What to say:

“Builder Pattern is a creational design pattern that lets you construct complex objects step by step. Instead of passing many parameters to a constructor, you use a builder with method chaining to set properties one at a time.”

Why it matters:

  • Shows you understand the fundamental purpose
  • Demonstrates knowledge of creational patterns category
  • Indicates you can explain concepts clearly

Must mention:

  • Many optional parameters - 5+ optional parameters
  • Complex construction - Objects need step-by-step setup
  • Readability - Want self-documenting code
  • Validation - Need to validate before creation
  • Avoiding telescoping constructors - Multiple constructors are messy

Example scenario to give:

“I’d use Builder Pattern when building HTTP requests or database query objects. These objects have many optional parameters like headers, query params, timeout, retries, etc. With Builder Pattern, the code reads like English and you only set what you need.”

Must explain:

  1. Product - The object being built
  2. Builder - Constructs the product step by step
  3. Method chaining - Each method returns builder
  4. build() - Validates and returns the product

Visual explanation:

Client → Builder.with_x().with_y().build() → Product

Benefits to mention:

  • Readability - Code reads like English sentences
  • Flexibility - Only set what you need
  • Validation - Can validate before creation
  • Extensibility - Easy to add new options
  • Self-documenting - Method names explain themselves

Trade-offs to acknowledge:

  • Complexity - Adds builder class (may be overkill for simple cases)
  • Performance - Small overhead (usually negligible)
  • Over-engineering risk - Don’t use for simple objects

Q: “What’s the difference between Builder Pattern and Factory Pattern?”

A:

“Factory Pattern creates objects of different types based on input (like creating StripePayment vs PayPalPayment). Builder Pattern constructs a single complex object step by step (like building a Pizza with size, crust, toppings). Factory chooses what to create, Builder constructs how to create.”

Q: “When would you NOT use Builder Pattern?”

A:

“I wouldn’t use Builder Pattern for simple objects with 1-3 parameters - a constructor is simpler. Also, if all parameters are required, a constructor is fine. I’d avoid it if performance is critical, though usually the overhead is negligible.”

Q: “How does Builder Pattern relate to SOLID principles?”

A:

“Builder Pattern supports Single Responsibility Principle by separating construction logic from the product class. It supports Open/Closed Principle - you can add new builder methods without modifying existing code. It also helps with readability, making code easier to understand and maintain.”

Key implementation points:

  1. Method chaining - Each method returns builder

    def with_size(self, size: str) -> 'PizzaBuilder':
    self.pizza.size = size
    return self # Return self for chaining
  2. Validation in build() - Validate before returning

    def build(self) -> Pizza:
    if not self.pizza.size:
    raise ValueError("Size is required")
    return self.pizza
  3. Immutable after build - Product shouldn’t change after building

    # Product should be immutable or builder shouldn't modify it
  4. Clear method names - Methods should be self-documenting

    .with_size("large") # Clear what it does
    .add_topping("pepperoni") # Clear what it does

Good examples to mention:

  • HTTP Requests - Headers, params, body, auth, timeout
  • Database Queries - SELECT, WHERE, JOIN, ORDER BY clauses
  • Email Messages - To, CC, BCC, subject, body, attachments
  • Configuration Objects - Many optional settings
  • UI Components - Many styling and behavior options

Mistakes interviewers watch for:

  1. Over-engineering - Using Builder for simple cases

    • ❌ Bad: Builder for Point(x, y)
    • ✅ Good: Builder for HttpRequest with 10+ options
  2. No validation - Building invalid objects

    • ❌ Bad: build() returns object without validation
    • ✅ Good: build() validates before returning
  3. Modifying built objects - Builder modifying already-built objects

    • ❌ Bad: Builder reusing same object instance
    • ✅ Good: Builder creates new object each time
  4. Inconsistent method names - Methods don’t follow naming convention

    • ❌ Bad: setSize(), addTopping(), configureCrust()
    • ✅ Good: with_size(), add_topping(), with_crust()

Builder vs Factory:

  • Builder - Constructs one complex object step by step
  • Factory - Creates objects of different types

Builder vs Constructor:

  • Builder - Step-by-step construction with validation
  • Constructor - Direct instantiation with parameters

Builder vs Prototype:

  • Builder - Constructs new objects step by step
  • Prototype - Clones existing objects

What interviewers look for:

Clean code - Readable, well-structured
Type hints - Proper type annotations
Error handling - Validates input, raises exceptions
Documentation - Clear docstrings
SOLID principles - Follows design principles
Testability - Easy to test and mock

Example of good code:

from typing import Optional
class HttpRequest:
"""HTTP request product"""
def __init__(self):
self.method: Optional[str] = None
self.url: Optional[str] = None
self.headers: dict = {}
class HttpRequestBuilder:
"""Builder for constructing HTTP requests"""
def __init__(self):
self.request = HttpRequest()
def with_method(self, method: str) -> 'HttpRequestBuilder':
"""
Set HTTP method.
Args:
method: HTTP method (GET, POST, etc.)
Returns:
Builder instance for method chaining
"""
self.request.method = method.upper()
return self
def build(self) -> HttpRequest:
"""
Build and return the HTTP request.
Returns:
HttpRequest instance
Raises:
ValueError: If required fields are missing
"""
if not self.request.method:
raise ValueError("HTTP method is required")
if not self.request.url:
raise ValueError("URL is required")
return self.request

Before your interview, make sure you can:

  • Define Builder Pattern clearly in one sentence
  • Explain when to use it (with examples)
  • Describe the structure and components
  • List benefits and trade-offs
  • Compare with other creational patterns
  • Implement Builder Pattern from scratch
  • Connect to SOLID principles
  • Identify when NOT to use it
  • Give 2-3 real-world examples
  • Discuss common mistakes and how to avoid them

Remember: Builder Pattern is about constructing complex objects step by step with clarity and flexibility! 🏗️