Builder

The Builder Pattern is a creational design pattern that provides a flexible solution for constructing complex objects step by step. Rather than creating objects directly through constructors with multiple parameters, the Builder Pattern separates the construction process from the object representation, allowing the same construction process to create different representations of an object.

Problem Statement

Traditional constructors can become unwieldy when creating complex objects requiring numerous parameters or having multiple optional configurations. Consider these common issues:

  • Telescoping Constructor Problem: Multiple constructor overloads with different parameter combinations become difficult to manage and use
  • Parameter Order Confusion: Long parameter lists are error-prone and hard to remember
  • Optional Parameters: Handling objects with many optional attributes becomes cumbersome
  • Immutable Objects: Creating immutable objects with complex initialization logic

Solution Approach

The Builder Pattern addresses these issues by:

  1. Separating Construction Logic: Moving object creation logic into a dedicated builder class
  2. Step-by-Step Construction: Building objects incrementally through method chaining
  3. Flexible Configuration: Allowing different combinations of parameters without multiple constructors
  4. Clear Interface: Providing a readable and intuitive object creation syntax

Core Components

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│    Director     │    │     Builder     │    │     Product     │
│                 │    │   (Abstract)    │    │                 │
│ - construct()   │───▶│ + build_part_a()│    │ - part_a        │
│                 │    │ + build_part_b()│    │ - part_b        │
│                 │    │ + get_result()  │    │ - part_c        │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                              △                        △
                              │                        │
                              │                        │
                       ┌─────────────────┐             │
                       │ ConcreteBuilder │             │
                       │                 │             │
                       │ + build_part_a()│─────────────┘
                       │ + build_part_b()│
                       │ + get_result()  │
                       └─────────────────┘

Component Roles:

  • Product: The complex object being constructed
  • Builder: Abstract interface defining construction steps
  • ConcreteBuilder: Implements the construction steps and maintains the product instance
  • Director: Orchestrates the construction process using the builder interface

Implementation Patterns

Method Chaining Builder

The most common implementation uses method chaining for fluent interface design, where each builder method returns the builder instance itself.

Step Builder

A variation that enforces the order of construction steps through interfaces, ensuring required parameters are set before optional ones.

from abc import ABC, abstractmethod
from typing import Optional, List
from dataclasses import dataclass

@dataclass(frozen=True)
class EmailMessage:
    """
    Immutable email message object created by the Step Builder.
    Uses dataclass with frozen=True to ensure immutability after creation.
    """
    sender: str
    recipients: List[str]
    subject: str
    body: str
    cc_recipients: Optional[List[str]] = None
    bcc_recipients: Optional[List[str]] = None
    attachments: Optional[List[str]] = None
    priority: str = "normal"
    html_body: Optional[str] = None
    
    def __str__(self) -> str:
        """String representation of the email message."""
        lines = [
            f"From: {self.sender}",
            f"To: {', '.join(self.recipients)}",
        ]
        
        if self.cc_recipients:
            lines.append(f"CC: {', '.join(self.cc_recipients)}")
        if self.bcc_recipients:
            lines.append(f"BCC: {', '.join(self.bcc_recipients)}")
        
        lines.extend([
            f"Subject: {self.subject}",
            f"Priority: {self.priority}",
            "",
            "Body:",
            self.body
        ])
        
        if self.html_body:
            lines.extend(["", "HTML Body:", self.html_body])
        
        if self.attachments:
            lines.extend(["", f"Attachments: {', '.join(self.attachments)}"])
        
        return "\n".join(lines)

class SenderStep(ABC):
    """First step: Setting the sender (required)."""
    
    @abstractmethod
    def sender(self, email: str) -> 'RecipientsStep':
        """Set the sender email address."""
        pass

class RecipientsStep(ABC):
    """Second step: Setting recipients (required)."""
    
    @abstractmethod
    def to(self, *recipients: str) -> 'SubjectStep':
        """Set the primary recipients."""
        pass

class SubjectStep(ABC):
    """Third step: Setting the subject (required)."""
    
    @abstractmethod
    def subject(self, subject: str) -> 'BodyStep':
        """Set the email subject."""
        pass

class BodyStep(ABC):
    """Fourth step: Setting the body (required)."""
    
    @abstractmethod
    def body(self, body: str) -> 'OptionalStep':
        """Set the email body text."""
        pass

class OptionalStep(ABC):
    """Final step: Optional configurations and building."""
    
    @abstractmethod
    def cc(self, *recipients: str) -> 'OptionalStep':
        """Add CC recipients."""
        pass
    
    @abstractmethod
    def bcc(self, *recipients: str) -> 'OptionalStep':
        """Add BCC recipients."""
        pass
    
    @abstractmethod
    def attach(self, *filenames: str) -> 'OptionalStep':
        """Add file attachments."""
        pass
    
    @abstractmethod
    def priority(self, priority: str) -> 'OptionalStep':
        """Set email priority (low, normal, high)."""
        pass
    
    @abstractmethod
    def html_body(self, html: str) -> 'OptionalStep':
        """Set HTML version of the email body."""
        pass
    
    @abstractmethod
    def build(self) -> EmailMessage:
        """Build and return the final email message."""
        pass

class EmailBuilder(SenderStep, RecipientsStep, SubjectStep, BodyStep, OptionalStep):
    """
    Step Builder implementation that enforces the order of required parameters
    through interface progression. Each step returns the interface for the next step.
    """
    
    def __init__(self) -> None:
        # Required fields
        self._sender: Optional[str] = None
        self._recipients: List[str] = []
        self._subject: Optional[str] = None
        self._body: Optional[str] = None
        
        # Optional fields with defaults
        self._cc_recipients: List[str] = []
        self._bcc_recipients: List[str] = []
        self._attachments: List[str] = []
        self._priority: str = "normal"
        self._html_body: Optional[str] = None
    
    @staticmethod
    def create() -> 'SenderStep':
        """Static factory method to start the building process."""
        return EmailBuilder()
    
    def sender(self, email: str) -> 'RecipientsStep':
        """Set the sender email address and return the next step."""
        if not email or "@" not in email:
            raise ValueError("Valid sender email is required")
        self._sender = email
        return self
    
    def to(self, *recipients: str) -> 'SubjectStep':
        """Set the primary recipients and return the next step."""
        if not recipients:
            raise ValueError("At least one recipient is required")
        for recipient in recipients:
            if not recipient or "@" not in recipient:
                raise ValueError(f"Invalid recipient email: {recipient}")
        self._recipients = list(recipients)
        return self
    
    def subject(self, subject: str) -> 'BodyStep':
        """Set the email subject and return the next step."""
        if not subject.strip():
            raise ValueError("Subject cannot be empty")
        self._subject = subject
        return self
    
    def body(self, body: str) -> 'OptionalStep':
        """Set the email body and return the optional configuration step."""
        if not body.strip():
            raise ValueError("Body cannot be empty")
        self._body = body
        return self
    
    def cc(self, *recipients: str) -> 'OptionalStep':
        """Add CC recipients (optional)."""
        for recipient in recipients:
            if recipient and "@" in recipient:
                self._cc_recipients.append(recipient)
        return self
    
    def bcc(self, *recipients: str) -> 'OptionalStep':
        """Add BCC recipients (optional)."""
        for recipient in recipients:
            if recipient and "@" in recipient:
                self._bcc_recipients.append(recipient)
        return self
    
    def attach(self, *filenames: str) -> 'OptionalStep':
        """Add file attachments (optional)."""
        self._attachments.extend(filenames)
        return self
    
    def priority(self, priority: str) -> 'OptionalStep':
        """Set email priority (optional)."""
        valid_priorities = ["low", "normal", "high"]
        if priority.lower() in valid_priorities:
            self._priority = priority.lower()
        else:
            raise ValueError(f"Priority must be one of: {valid_priorities}")
        return self
    
    def html_body(self, html: str) -> 'OptionalStep':
        """Set HTML version of the email body (optional)."""
        self._html_body = html
        return self
    
    def build(self) -> EmailMessage:
        """Build and return the final immutable email message."""
        # Final validation
        if not all([self._sender, self._recipients, self._subject, self._body]):
            raise ValueError("All required fields must be set before building")
        
        # Create the immutable email message
        return EmailMessage(
            sender=self._sender,
            recipients=self._recipients.copy(),
            subject=self._subject,
            body=self._body,
            cc_recipients=self._cc_recipients.copy() if self._cc_recipients else None,
            bcc_recipients=self._bcc_recipients.copy() if self._bcc_recipients else None,
            attachments=self._attachments.copy() if self._attachments else None,
            priority=self._priority,
            html_body=self._html_body
        )
# Example usage demonstrating the Step Builder pattern
if __name__ == "__main__":
    # Example 1: Simple email with only required fields
    simple_email = (EmailBuilder.create()
                   .sender("john.doe@company.com")
                   .to("jane.smith@company.com")
                   .subject("Project Update")
                   .body("Hi Jane,\n\nHere's the latest update on our project.\n\nBest regards,\nJohn")
                   .build())
    
    print("Simple Email:")
    print(simple_email)
    print("\n" + "="*70 + "\n")
    
    # Example 2: Complex email with all optional features
    complex_email = (EmailBuilder.create()
                    .sender("project.manager@company.com")
                    .to("team.lead@company.com", "developer@company.com")
                    .subject("Urgent: Production Issue")
                    .body("Team,\n\nWe have a critical production issue that needs immediate attention.")
                    .cc("cto@company.com", "ops.manager@company.com")
                    .bcc("audit@company.com")
                    .priority("high")
                    .attach("error_logs.txt", "system_status.pdf")
                    .html_body("<h2>Urgent: Production Issue</h2><p>We have a critical production issue.</p>")
                    .build())
    
    print("Complex Email:")
    print(complex_email)
    print("\n" + "="*70 + "\n")
    
    # Example 3: Demonstrate validation - this will show how the step builder prevents errors
    try:
        # This would fail at compile time in strongly typed languages
        # In Python, it will fail at runtime with clear error messages
        invalid_email = (EmailBuilder.create()
                         .sender("invalid-email")  # Invalid email format
                         .to("recipient@company.com")
                         .subject("Test")
                         .body("Test message")
                         .build())
    except ValueError as e:
        print(f"Validation Error: {e}")

Telescoping Builder

Combines traditional constructors with builder pattern benefits, useful when some parameters are always required.

from abc import ABC, abstractmethod
from typing import Optional

class DatabaseConnection:
    """
    Product class representing a database connection configuration.
    This demonstrates a more complex object with multiple configuration options.
    """
    
    def __init__(self) -> None:
        # Connection details
        self.host: Optional[str] = None
        self.port: Optional[int] = None
        self.database: Optional[str] = None
        self.username: Optional[str] = None
        self.password: Optional[str] = None
        
        # Connection pool settings
        self.min_connections: int = 1
        self.max_connections: int = 10
        self.connection_timeout: int = 30
        
        # SSL and security settings
        self.ssl_enabled: bool = False
        self.ssl_cert_path: Optional[str] = None
        self.encryption_enabled: bool = False
        
        # Performance settings
        self.query_timeout: int = 60
        self.retry_attempts: int = 3
        self.connection_validation: bool = True
    
    def __str__(self) -> str:
        """String representation of the database configuration."""
        config_lines = [
            "Database Connection Configuration:",
            f"  Connection: {self.username}@{self.host}:{self.port}/{self.database}",
            f"  Pool: {self.min_connections}-{self.max_connections} connections",
            f"  Timeouts: connection={self.connection_timeout}s, query={self.query_timeout}s",
            f"  Security: SSL={self.ssl_enabled}, Encryption={self.encryption_enabled}",
            f"  Reliability: {self.retry_attempts} retries, validation={self.connection_validation}"
        ]
        return "\n".join(config_lines)

class DatabaseConnectionBuilder(ABC):
    """
    Abstract builder interface for database connections.
    Defines the contract for building database connection objects.
    """
    
    @abstractmethod
    def reset(self) -> None:
        """Reset the builder to create a new connection configuration."""
        pass
    
    @abstractmethod
    def set_connection_details(self, host: str, port: int, database: str, 
                             username: str, password: str) -> None:
        """Set basic connection details."""
        pass
    
    @abstractmethod
    def set_connection_pool(self, min_conn: int, max_conn: int, timeout: int) -> None:
        """Configure connection pool settings."""
        pass
    
    @abstractmethod
    def set_security_options(self, ssl: bool, ssl_cert: Optional[str], 
                           encryption: bool) -> None:
        """Configure security settings."""
        pass
    
    @abstractmethod
    def set_performance_options(self, query_timeout: int, retry_attempts: int, 
                              validation: bool) -> None:
        """Configure performance and reliability settings."""
        pass
    
    @abstractmethod
    def get_result(self) -> DatabaseConnection:
        """Return the built database connection configuration."""
        pass

class ConcreteConnectionBuilder(DatabaseConnectionBuilder):
    """
    Concrete implementation of the database connection builder.
    Creates and configures DatabaseConnection objects.
    """
    
    def __init__(self) -> None:
        self.reset()
    
    def reset(self) -> None:
        """Reset the builder with a new DatabaseConnection instance."""
        self._connection = DatabaseConnection()
    
    def set_connection_details(self, host: str, port: int, database: str, 
                             username: str, password: str) -> None:
        """Set the basic connection parameters."""
        self._connection.host = host
        self._connection.port = port
        self._connection.database = database
        self._connection.username = username
        self._connection.password = password
    
    def set_connection_pool(self, min_conn: int, max_conn: int, timeout: int) -> None:
        """Configure the connection pool settings."""
        self._connection.min_connections = min_conn
        self._connection.max_connections = max_conn
        self._connection.connection_timeout = timeout
    
    def set_security_options(self, ssl: bool, ssl_cert: Optional[str] = None, 
                           encryption: bool = False) -> None:
        """Configure security and encryption settings."""
        self._connection.ssl_enabled = ssl
        self._connection.ssl_cert_path = ssl_cert
        self._connection.encryption_enabled = encryption
    
    def set_performance_options(self, query_timeout: int, retry_attempts: int, 
                              validation: bool) -> None:
        """Configure performance and reliability settings."""
        self._connection.query_timeout = query_timeout
        self._connection.retry_attempts = retry_attempts
        self._connection.connection_validation = validation
    
    def get_result(self) -> DatabaseConnection:
        """Return the configured database connection."""
        result = self._connection
        self.reset()  # Reset for next build
        return result

class DatabaseConnectionDirector:
    """
    Director class that orchestrates the construction of different types of database connections.
    Encapsulates the construction logic for common database configuration patterns.
    """
    
    def __init__(self, builder: DatabaseConnectionBuilder) -> None:
        self._builder = builder
    
    def build_development_connection(self, host: str = "localhost", 
                                   database: str = "dev_db") -> DatabaseConnection:
        """
        Build a development database connection with relaxed security and small pool.
        """
        self._builder.reset()
        self._builder.set_connection_details(
            host=host, 
            port=5432, 
            database=database,
            username="dev_user", 
            password="dev_password"
        )
        self._builder.set_connection_pool(min_conn=1, max_conn=5, timeout=10)
        self._builder.set_security_options(ssl=False, encryption=False)
        self._builder.set_performance_options(query_timeout=30, retry_attempts=1, validation=False)
        return self._builder.get_result()
    
    def build_production_connection(self, host: str, database: str, 
                                  username: str, password: str) -> DatabaseConnection:
        """
        Build a production database connection with high security and large pool.
        """
        self._builder.reset()
        self._builder.set_connection_details(
            host=host, 
            port=5432, 
            database=database,
            username=username, 
            password=password
        )
        self._builder.set_connection_pool(min_conn=5, max_conn=50, timeout=60)
        self._builder.set_security_options(ssl=True, ssl_cert="/etc/ssl/certs/db.pem", encryption=True)
        self._builder.set_performance_options(query_timeout=120, retry_attempts=5, validation=True)
        return self._builder.get_result()
    
    def build_testing_connection(self, database: str = "test_db") -> DatabaseConnection:
        """
        Build a testing database connection optimized for fast test execution.
        """
        self._builder.reset()
        self._builder.set_connection_details(
            host="localhost", 
            port=5432, 
            database=database,
            username="test_user", 
            password="test_password"
        )
        self._builder.set_connection_pool(min_conn=1, max_conn=3, timeout=5)
        self._builder.set_security_options(ssl=False, encryption=False)
        self._builder.set_performance_options(query_timeout=10, retry_attempts=2, validation=False)
        return self._builder.get_result()

# Example usage demonstrating the Builder pattern with Director
if __name__ == "__main__":
    # Create builder and director
    builder = ConcreteConnectionBuilder()
    director = DatabaseConnectionDirector(builder)
    
    # Build different types of database connections
    print("Development Connection:")
    dev_connection = director.build_development_connection()
    print(dev_connection)
    print("\n" + "="*60 + "\n")
    
    print("Production Connection:")
    prod_connection = director.build_production_connection(
        host="prod-db.company.com",
        database="production_db",
        username="prod_user",
        password="secure_password"
    )
    print(prod_connection)
    print("\n" + "="*60 + "\n")
    
    print("Testing Connection:")
    test_connection = director.build_testing_connection("integration_test_db")
    print(test_connection)
    print("\n" + "="*60 + "\n")
    
    # Build custom connection using builder directly
    print("Custom Connection (using builder directly):")
    custom_connection = builder
    custom_connection.set_connection_details(
        host="analytics-db.company.com",
        port=5432,
        database="analytics",
        username="analyst",
        password="analyst_pass"
    )
    custom_connection.set_connection_pool(min_conn=2, max_conn=20, timeout=45)
    custom_connection.set_security_options(ssl=True, encryption=True)
    custom_connection.set_performance_options(query_timeout=300, retry_attempts=3, validation=True)
    
    custom_result = custom_connection.get_result()
    print(custom_result)

Benefits

  • Readability: The Builder pattern creates self-documenting code where the construction process is clear and intuitive.
  • Flexibility: Easy to add new optional parameters without breaking existing code or creating multiple constructor overloads.
  • Immutability: Enables creation of immutable objects by building them completely before returning the final instance.
  • Validation: Centralized validation logic in the build method ensures object consistency before creation.
  • Reusability: Builder instances can be reused and configured differently to create multiple objects.

Considerations

  • Increased Complexity: Introduces additional classes and interfaces, which may be overkill for simple objects.
  • Memory Overhead: Builder instances consume additional memory, though this is typically negligible.
  • Learning Curve: Team members need to understand the pattern to use and maintain the code effectively.

When to Use

The Builder Pattern is most beneficial when:

  • Objects have four or more constructor parameters
  • Multiple optional parameters need to be handled elegantly
  • Object creation logic is complex and needs to be separated from the object itself
  • You need to create different representations of the same object
  • Immutable objects require complex initialization
  • Step-by-step construction provides better control over the creation process

Track your progress

Mark this subtopic as completed when you finish reading.