Builder Pattern - Constructing Complex Objects Step by Step
Learning Objectives: By the end of this lesson, you will be able to:
- Understand the Builder pattern and when to use it for complex object construction
- Implement fluent interfaces and method chaining for intuitive object building
- Create immutable objects using the Builder pattern for thread safety
- Apply Builder pattern to real-world scenarios like configuration objects and API requests
- Recognize the difference between Builder pattern and telescoping constructors
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Think about building a custom computer or ordering a complex sandwich with many options. You don’t want to specify every possible parameter at once - instead, you choose components step by step. How is this similar to creating complex objects in programming?
Lesson Overview
- Duration: 120 minutes
- Prerequisites: Object-oriented programming, method chaining concepts, basic understanding of immutability
- Materials Needed: Development environment with Java, Python, or JavaScript support
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Problem Definition - Issues with telescoping constructors and complex object creation
- Builder Pattern Structure - Director, Builder, and Product components
- Implementation Approaches - Classic Builder, Fluent Builder, and Step Builder
- Real-world Applications - HTTP request builders, configuration objects, SQL query builders
- Best Practices - Immutability, validation, and method chaining design
- Modern Usage - Lombok @Builder, JavaScript builders, and framework examples
Success Criteria:
By the end of this session, you should be able to implement elegant Builder patterns that make complex object creation intuitive and maintainable.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
What is the Builder Pattern?
The Builder pattern is a creational design pattern that constructs complex objects step by step. It allows you to produce different types and representations of an object using the same construction code, providing a clean and intuitive way to build objects with many parameters.
Real-World Analogy: Consider building a house. You don’t construct it all at once - instead, you work with architects and contractors who specialize in different aspects: foundation, framing, plumbing, electrical, and finishing. Each specialist adds their component step by step until the house is complete.
Why This Matters:
- Readability: Makes complex object creation code self-documenting
- Flexibility: Allows optional parameters without multiple constructors
- Immutability: Can create immutable objects safely
- Validation: Enables step-by-step validation during construction
Problems Builder Pattern Solves
Problem 1: Telescoping Constructor Anti-pattern
// Bad: Too many constructors with different parameter combinations
public class Pizza {
public Pizza(String dough) { /* ... */ }
public Pizza(String dough, String sauce) { /* ... */ }
public Pizza(String dough, String sauce, String cheese) { /* ... */ }
public Pizza(String dough, String sauce, String cheese, String meat) { /* ... */ }
public Pizza(String dough, String sauce, String cheese, String meat, String vegetables) { /* ... */ }
// ... many more constructors
// Client code becomes confusing
Pizza pizza = new Pizza("thin", "tomato", "mozzarella", null, "peppers");
// What does null mean? Which parameter is which?
}
Problem 2: Long Parameter Lists
// Bad: Constructor with many parameters
public class DatabaseConfig {
public DatabaseConfig(String host, int port, String database, String username,
String password, boolean useSSL, int timeout, int maxConnections,
boolean autoReconnect, String charset, boolean cache) {
// Which parameter is which? Easy to mix up boolean values!
}
}
// Confusing client code
DatabaseConfig config = new DatabaseConfig("localhost", 5432, "mydb", "user",
"pass", true, 30, 10, false, "UTF-8", true);
Problem 3: Mutable Objects with Inconsistent State
// Bad: Object can be in inconsistent state
public class HttpRequest {
private String url;
private String method;
private Map<String, String> headers;
// Object can be used before all required fields are set
HttpRequest request = new HttpRequest();
request.setUrl("https://api.example.com");
// Oops! Method not set, object is in invalid state
request.send(); // This could fail unexpectedly
}
Key Terminology
- Builder: Interface or abstract class that defines steps for constructing objects
- Concrete Builder: Implements the Builder interface for specific product variants
- Director: Orchestrates the construction process using a Builder
- Product: The complex object being constructed
- Fluent Interface: Method chaining that reads like natural language
- Step Builder: Builder that enforces a specific order of construction steps
4. Detailed Explanation and Examples (25-30 minutes)
Understanding Builder Pattern Structure
Classic Builder Pattern (GoF)
// Product class
class Computer {
private final String cpu;
private final String ram;
private final String storage;
private final String gpu;
private final String motherboard;
private final boolean hasWifi;
private final boolean hasBluetooth;
// Private constructor - only Builder can create instances
private Computer(ComputerBuilder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
this.gpu = builder.gpu;
this.motherboard = builder.motherboard;
this.hasWifi = builder.hasWifi;
this.hasBluetooth = builder.hasBluetooth;
}
// Getters only - immutable object
public String getCpu() { return cpu; }
public String getRam() { return ram; }
public String getStorage() { return storage; }
public String getGpu() { return gpu; }
public String getMotherboard() { return motherboard; }
public boolean hasWifi() { return hasWifi; }
public boolean hasBluetooth() { return hasBluetooth; }
@Override
public String toString() {
return String.format("Computer{cpu='%s', ram='%s', storage='%s', gpu='%s', " +
"motherboard='%s', wifi=%s, bluetooth=%s}",
cpu, ram, storage, gpu, motherboard, hasWifi, hasBluetooth);
}
// Static inner Builder class
public static class ComputerBuilder {
// Required parameters
private final String cpu;
private final String ram;
// Optional parameters with default values
private String storage = "256GB SSD";
private String gpu = "Integrated";
private String motherboard = "Standard";
private boolean hasWifi = true;
private boolean hasBluetooth = false;
// Constructor for required parameters
public ComputerBuilder(String cpu, String ram) {
this.cpu = cpu;
this.ram = ram;
}
// Fluent setter methods for optional parameters
public ComputerBuilder storage(String storage) {
this.storage = storage;
return this;
}
public ComputerBuilder gpu(String gpu) {
this.gpu = gpu;
return this;
}
public ComputerBuilder motherboard(String motherboard) {
this.motherboard = motherboard;
return this;
}
public ComputerBuilder wifi(boolean hasWifi) {
this.hasWifi = hasWifi;
return this;
}
public ComputerBuilder bluetooth(boolean hasBluetooth) {
this.hasBluetooth = hasBluetooth;
return this;
}
// Build method creates the final object
public Computer build() {
// Validation can be performed here
if (cpu == null || cpu.trim().isEmpty()) {
throw new IllegalStateException("CPU is required");
}
if (ram == null || ram.trim().isEmpty()) {
throw new IllegalStateException("RAM is required");
}
return new Computer(this);
}
}
}
// Usage examples
public class ComputerBuilderDemo {
public static void main(String[] args) {
// Basic computer with required parameters only
Computer basicComputer = new Computer.ComputerBuilder("Intel i5", "8GB")
.build();
// Gaming computer with custom configuration
Computer gamingComputer = new Computer.ComputerBuilder("Intel i9", "32GB")
.storage("1TB NVMe SSD")
.gpu("RTX 4080")
.motherboard("Gaming X570")
.wifi(true)
.bluetooth(true)
.build();
// Workstation computer
Computer workstation = new Computer.ComputerBuilder("AMD Ryzen 9", "64GB")
.storage("2TB SSD")
.gpu("RTX A4000")
.motherboard("Pro WS X570")
.build();
System.out.println(basicComputer);
System.out.println(gamingComputer);
System.out.println(workstation);
}
}
Fluent Builder with Method Chaining
from typing import Optional, Dict, Any
from datetime import datetime
class HttpRequest:
def __init__(self, builder: 'HttpRequestBuilder'):
self.url = builder._url
self.method = builder._method
self.headers = builder._headers.copy()
self.params = builder._params.copy()
self.body = builder._body
self.timeout = builder._timeout
self.auth = builder._auth
def __str__(self):
return f"HttpRequest(method={self.method}, url={self.url}, headers={len(self.headers)} items)"
class HttpRequestBuilder:
def __init__(self):
self._url: Optional[str] = None
self._method: str = "GET"
self._headers: Dict[str, str] = {}
self._params: Dict[str, str] = {}
self._body: Optional[Any] = None
self._timeout: int = 30
self._auth: Optional[tuple] = None
def url(self, url: str) -> 'HttpRequestBuilder':
"""Set the request URL"""
if not url or not url.strip():
raise ValueError("URL cannot be empty")
self._url = url.strip()
return self
def method(self, method: str) -> 'HttpRequestBuilder':
"""Set the HTTP method"""
valid_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
method = method.upper()
if method not in valid_methods:
raise ValueError(f"Invalid HTTP method: {method}")
self._method = method
return self
def header(self, key: str, value: str) -> 'HttpRequestBuilder':
"""Add a single header"""
self._headers[key] = value
return self
def headers(self, headers: Dict[str, str]) -> 'HttpRequestBuilder':
"""Add multiple headers"""
self._headers.update(headers)
return self
def param(self, key: str, value: str) -> 'HttpRequestBuilder':
"""Add a single query parameter"""
self._params[key] = value
return self
def params(self, params: Dict[str, str]) -> 'HttpRequestBuilder':
"""Add multiple query parameters"""
self._params.update(params)
return self
def body(self, body: Any) -> 'HttpRequestBuilder':
"""Set the request body"""
self._body = body
return self
def json(self, data: dict) -> 'HttpRequestBuilder':
"""Set JSON body and appropriate header"""
import json
self._body = json.dumps(data)
self._headers["Content-Type"] = "application/json"
return self
def timeout(self, timeout: int) -> 'HttpRequestBuilder':
"""Set request timeout in seconds"""
if timeout <= 0:
raise ValueError("Timeout must be positive")
self._timeout = timeout
return self
def auth(self, username: str, password: str) -> 'HttpRequestBuilder':
"""Set basic authentication"""
self._auth = (username, password)
return self
def user_agent(self, agent: str) -> 'HttpRequestBuilder':
"""Set User-Agent header"""
return self.header("User-Agent", agent)
def accept_json(self) -> 'HttpRequestBuilder':
"""Set Accept header for JSON"""
return self.header("Accept", "application/json")
def build(self) -> HttpRequest:
"""Build the final HttpRequest object"""
if not self._url:
raise ValueError("URL is required")
# Add timestamp header for tracking
self._headers["X-Request-Time"] = datetime.now().isoformat()
return HttpRequest(self)
# Usage examples
def demonstrate_http_builder():
# Simple GET request
simple_request = (HttpRequestBuilder()
.url("https://api.example.com/users")
.method("GET")
.accept_json()
.build())
# Complex POST request with authentication
complex_request = (HttpRequestBuilder()
.url("https://api.example.com/users")
.method("POST")
.auth("admin", "secret123")
.json({
"name": "John Doe",
"email": "john@example.com",
"role": "user"
})
.header("X-API-Version", "1.0")
.timeout(60)
.build())
# API request with query parameters
search_request = (HttpRequestBuilder()
.url("https://api.example.com/search")
.method("GET")
.params({
"q": "design patterns",
"limit": "10",
"offset": "0"
})
.header("Authorization", "Bearer token123")
.user_agent("MyApp/1.0")
.build())
print(simple_request)
print(complex_request)
print(search_request)
demonstrate_http_builder()
Step Builder Pattern (Enforcing Order)
// Step Builder ensures methods are called in the correct order
public class DatabaseConnection {
private final String host;
private final int port;
private final String database;
private final String username;
private final String password;
private final boolean useSSL;
private final int timeout;
private DatabaseConnection(Builder builder) {
this.host = builder.host;
this.port = builder.port;
this.database = builder.database;
this.username = builder.username;
this.password = builder.password;
this.useSSL = builder.useSSL;
this.timeout = builder.timeout;
}
// Step interfaces ensure proper order
public interface HostStep {
PortStep host(String host);
}
public interface PortStep {
DatabaseStep port(int port);
}
public interface DatabaseStep {
UsernameStep database(String database);
}
public interface UsernameStep {
PasswordStep username(String username);
}
public interface PasswordStep {
OptionalStep password(String password);
}
public interface OptionalStep {
OptionalStep ssl(boolean useSSL);
OptionalStep timeout(int timeout);
DatabaseConnection build();
}
public static class Builder implements HostStep, PortStep, DatabaseStep,
UsernameStep, PasswordStep, OptionalStep {
private String host;
private int port;
private String database;
private String username;
private String password;
private boolean useSSL = false;
private int timeout = 30;
@Override
public PortStep host(String host) {
this.host = host;
return this;
}
@Override
public DatabaseStep port(int port) {
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Port must be between 1 and 65535");
}
this.port = port;
return this;
}
@Override
public UsernameStep database(String database) {
this.database = database;
return this;
}
@Override
public PasswordStep username(String username) {
this.username = username;
return this;
}
@Override
public OptionalStep password(String password) {
this.password = password;
return this;
}
@Override
public OptionalStep ssl(boolean useSSL) {
this.useSSL = useSSL;
return this;
}
@Override
public OptionalStep timeout(int timeout) {
if (timeout <= 0) {
throw new IllegalArgumentException("Timeout must be positive");
}
this.timeout = timeout;
return this;
}
@Override
public DatabaseConnection build() {
return new DatabaseConnection(this);
}
}
public static HostStep builder() {
return new Builder();
}
public String getConnectionString() {
return String.format("postgresql://%s:%s@%s:%d/%s?ssl=%s&timeout=%d",
username, "***", host, port, database, useSSL, timeout);
}
}
// Usage - compiler enforces the order
DatabaseConnection connection = DatabaseConnection.builder()
.host("localhost") // Must be first
.port(5432) // Must be second
.database("myapp") // Must be third
.username("dbuser") // Must be fourth
.password("secret") // Must be fifth
.ssl(true) // Optional - can be in any order
.timeout(60) // Optional - can be in any order
.build(); // Must be last
Practical Applications
SQL Query Builder
class SQLQueryBuilder {
constructor() {
this._type = null;
this._table = null;
this._columns = [];
this._conditions = [];
this._joins = [];
this._orderBy = [];
this._groupBy = [];
this._having = [];
this._limit = null;
this._offset = null;
this._values = {};
this._updates = {};
}
// SELECT operations
select(columns = '*') {
this._type = 'SELECT';
this._columns = Array.isArray(columns) ? columns : [columns];
return this;
}
from(table) {
this._table = table;
return this;
}
where(condition, value = null) {
if (value !== null) {
condition = condition.replace('?', `'${value}'`);
}
this._conditions.push(condition);
return this;
}
join(table, condition) {
this._joins.push(`JOIN ${table} ON ${condition}`);
return this;
}
leftJoin(table, condition) {
this._joins.push(`LEFT JOIN ${table} ON ${condition}`);
return this;
}
orderBy(column, direction = 'ASC') {
this._orderBy.push(`${column} ${direction}`);
return this;
}
groupBy(columns) {
const cols = Array.isArray(columns) ? columns : [columns];
this._groupBy.push(...cols);
return this;
}
having(condition) {
this._having.push(condition);
return this;
}
limit(count) {
this._limit = count;
return this;
}
offset(count) {
this._offset = count;
return this;
}
// INSERT operations
insertInto(table) {
this._type = 'INSERT';
this._table = table;
return this;
}
values(valueObject) {
this._values = { ...this._values, ...valueObject };
return this;
}
// UPDATE operations
update(table) {
this._type = 'UPDATE';
this._table = table;
return this;
}
set(updates) {
this._updates = { ...this._updates, ...updates };
return this;
}
// DELETE operations
deleteFrom(table) {
this._type = 'DELETE';
this._table = table;
return this;
}
// Build the final SQL query
build() {
switch (this._type) {
case 'SELECT':
return this._buildSelect();
case 'INSERT':
return this._buildInsert();
case 'UPDATE':
return this._buildUpdate();
case 'DELETE':
return this._buildDelete();
default:
throw new Error('Query type not specified');
}
}
_buildSelect() {
let query = `SELECT ${this._columns.join(', ')} FROM ${this._table}`;
if (this._joins.length > 0) {
query += ` ${this._joins.join(' ')}`;
}
if (this._conditions.length > 0) {
query += ` WHERE ${this._conditions.join(' AND ')}`;
}
if (this._groupBy.length > 0) {
query += ` GROUP BY ${this._groupBy.join(', ')}`;
}
if (this._having.length > 0) {
query += ` HAVING ${this._having.join(' AND ')}`;
}
if (this._orderBy.length > 0) {
query += ` ORDER BY ${this._orderBy.join(', ')}`;
}
if (this._limit !== null) {
query += ` LIMIT ${this._limit}`;
}
if (this._offset !== null) {
query += ` OFFSET ${this._offset}`;
}
return query;
}
_buildInsert() {
const columns = Object.keys(this._values);
const values = Object.values(this._values).map(v =>
typeof v === 'string' ? `'${v}'` : v
);
return `INSERT INTO ${this._table} (${columns.join(', ')}) VALUES (${values.join(', ')})`;
}
_buildUpdate() {
const setParts = Object.entries(this._updates).map(([key, value]) =>
`${key} = ${typeof value === 'string' ? `'${value}'` : value}`
);
let query = `UPDATE ${this._table} SET ${setParts.join(', ')}`;
if (this._conditions.length > 0) {
query += ` WHERE ${this._conditions.join(' AND ')}`;
}
return query;
}
_buildDelete() {
let query = `DELETE FROM ${this._table}`;
if (this._conditions.length > 0) {
query += ` WHERE ${this._conditions.join(' AND ')}`;
}
return query;
}
}
// Usage examples
function demonstrateSQLBuilder() {
// Complex SELECT query
const selectQuery = new SQLQueryBuilder()
.select(['u.name', 'u.email', 'p.title'])
.from('users u')
.leftJoin('posts p', 'u.id = p.user_id')
.where('u.active = ?', true)
.where('u.created_at > ?', '2023-01-01')
.orderBy('u.name')
.limit(10)
.build();
console.log('SELECT Query:');
console.log(selectQuery);
console.log();
// INSERT query
const insertQuery = new SQLQueryBuilder()
.insertInto('users')
.values({
name: 'John Doe',
email: 'john@example.com',
age: 30,
active: true
})
.build();
console.log('INSERT Query:');
console.log(insertQuery);
console.log();
// UPDATE query
const updateQuery = new SQLQueryBuilder()
.update('users')
.set({
name: 'Jane Doe',
email: 'jane@example.com'
})
.where('id = ?', 1)
.build();
console.log('UPDATE Query:');
console.log(updateQuery);
console.log();
// DELETE query
const deleteQuery = new SQLQueryBuilder()
.deleteFrom('users')
.where('active = ?', false)
.where('last_login < ?', '2022-01-01')
.build();
console.log('DELETE Query:');
console.log(deleteQuery);
}
demonstrateSQLBuilder();
Common Misconceptions
-
Misconception: Builder pattern is only for objects with many parameters Reality: It’s also valuable for creating immutable objects and providing fluent interfaces
-
Misconception: All parameters should be settable through the builder Reality: Some parameters might be computed or have dependencies on others
-
Misconception: Builder pattern always creates immutable objects Reality: The pattern can create mutable objects, but immutability is a common best practice
5. Live Coding Demonstration (20-25 minutes)
Example 1: Configuration Builder with Validation
import os
from typing import Dict, Any, Optional
from dataclasses import dataclass
from enum import Enum
class LogLevel(Enum):
DEBUG = "DEBUG"
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
@dataclass(frozen=True) # Immutable configuration
class ApplicationConfig:
# Required settings
app_name: str
app_version: str
# Database settings
database_url: str
database_pool_size: int
# Server settings
host: str
port: int
# Optional settings with defaults
log_level: LogLevel = LogLevel.INFO
debug_mode: bool = False
max_request_size: int = 1024 * 1024 # 1MB
session_timeout: int = 3600 # 1 hour
cors_enabled: bool = False
cors_origins: tuple = ()
def __post_init__(self):
# Validation after object creation
if self.port < 1 or self.port > 65535:
raise ValueError(f"Invalid port: {self.port}")
if self.database_pool_size < 1:
raise ValueError("Database pool size must be positive")
if self.max_request_size < 1:
raise ValueError("Max request size must be positive")
class ApplicationConfigBuilder:
def __init__(self):
# Required fields (will be validated in build())
self._app_name: Optional[str] = None
self._app_version: Optional[str] = None
self._database_url: Optional[str] = None
self._database_pool_size: Optional[int] = None
self._host: Optional[str] = None
self._port: Optional[int] = None
# Optional fields with defaults
self._log_level: LogLevel = LogLevel.INFO
self._debug_mode: bool = False
self._max_request_size: int = 1024 * 1024
self._session_timeout: int = 3600
self._cors_enabled: bool = False
self._cors_origins: list = []
def app_name(self, name: str) -> 'ApplicationConfigBuilder':
if not name or not name.strip():
raise ValueError("App name cannot be empty")
self._app_name = name.strip()
return self
def app_version(self, version: str) -> 'ApplicationConfigBuilder':
if not version or not version.strip():
raise ValueError("App version cannot be empty")
self._app_version = version.strip()
return self
def database_url(self, url: str) -> 'ApplicationConfigBuilder':
if not url or not url.strip():
raise ValueError("Database URL cannot be empty")
# Basic URL validation
if not (url.startswith('postgresql://') or url.startswith('mysql://') or
url.startswith('sqlite:///')):
raise ValueError("Unsupported database URL format")
self._database_url = url.strip()
return self
def database_pool_size(self, size: int) -> 'ApplicationConfigBuilder':
if size < 1 or size > 100:
raise ValueError("Database pool size must be between 1 and 100")
self._database_pool_size = size
return self
def host(self, host: str) -> 'ApplicationConfigBuilder':
if not host or not host.strip():
raise ValueError("Host cannot be empty")
self._host = host.strip()
return self
def port(self, port: int) -> 'ApplicationConfigBuilder':
if port < 1 or port > 65535:
raise ValueError("Port must be between 1 and 65535")
self._port = port
return self
def log_level(self, level: LogLevel) -> 'ApplicationConfigBuilder':
self._log_level = level
return self
def debug_mode(self, debug: bool = True) -> 'ApplicationConfigBuilder':
self._debug_mode = debug
if debug:
self._log_level = LogLevel.DEBUG
return self
def max_request_size(self, size: int) -> 'ApplicationConfigBuilder':
if size < 1:
raise ValueError("Max request size must be positive")
self._max_request_size = size
return self
def session_timeout(self, timeout: int) -> 'ApplicationConfigBuilder':
if timeout < 1:
raise ValueError("Session timeout must be positive")
self._session_timeout = timeout
return self
def enable_cors(self, origins: list = None) -> 'ApplicationConfigBuilder':
self._cors_enabled = True
if origins:
self._cors_origins = origins.copy()
return self
def disable_cors(self) -> 'ApplicationConfigBuilder':
self._cors_enabled = False
self._cors_origins = []
return self
def from_environment(self) -> 'ApplicationConfigBuilder':
"""Load configuration from environment variables"""
if app_name := os.getenv('APP_NAME'):
self.app_name(app_name)
if app_version := os.getenv('APP_VERSION'):
self.app_version(app_version)
if db_url := os.getenv('DATABASE_URL'):
self.database_url(db_url)
if db_pool := os.getenv('DATABASE_POOL_SIZE'):
self.database_pool_size(int(db_pool))
if host := os.getenv('HOST'):
self.host(host)
if port := os.getenv('PORT'):
self.port(int(port))
if debug := os.getenv('DEBUG'):
self.debug_mode(debug.lower() in ('true', '1', 'yes'))
return self
def build(self) -> ApplicationConfig:
"""Build the final configuration object"""
# Validate required fields
required_fields = {
'app_name': self._app_name,
'app_version': self._app_version,
'database_url': self._database_url,
'database_pool_size': self._database_pool_size,
'host': self._host,
'port': self._port
}
missing_fields = [name for name, value in required_fields.items() if value is None]
if missing_fields:
raise ValueError(f"Missing required fields: {', '.join(missing_fields)}")
return ApplicationConfig(
app_name=self._app_name,
app_version=self._app_version,
database_url=self._database_url,
database_pool_size=self._database_pool_size,
host=self._host,
port=self._port,
log_level=self._log_level,
debug_mode=self._debug_mode,
max_request_size=self._max_request_size,
session_timeout=self._session_timeout,
cors_enabled=self._cors_enabled,
cors_origins=tuple(self._cors_origins)
)
# Usage examples
def demonstrate_config_builder():
# Production configuration
prod_config = (ApplicationConfigBuilder()
.app_name("MyWebApp")
.app_version("1.2.0")
.database_url("postgresql://user:pass@prod-db:5432/myapp")
.database_pool_size(20)
.host("0.0.0.0")
.port(8080)
.log_level(LogLevel.INFO)
.max_request_size(5 * 1024 * 1024) # 5MB
.session_timeout(7200) # 2 hours
.enable_cors(["https://myapp.com", "https://www.myapp.com"])
.build())
# Development configuration
dev_config = (ApplicationConfigBuilder()
.app_name("MyWebApp")
.app_version("1.2.0-dev")
.database_url("sqlite:///dev.db")
.database_pool_size(5)
.host("localhost")
.port(3000)
.debug_mode(True) # This also sets log level to DEBUG
.enable_cors() # Enable CORS for all origins in dev
.build())
# Configuration from environment
env_config = (ApplicationConfigBuilder()
.from_environment()
.app_name("MyWebApp") # Fallback if not in env
.app_version("1.0.0") # Fallback if not in env
.build())
print("Production Config:")
print(f" App: {prod_config.app_name} v{prod_config.app_version}")
print(f" Server: {prod_config.host}:{prod_config.port}")
print(f" Database: {prod_config.database_url}")
print(f" Debug: {prod_config.debug_mode}")
print(f" CORS: {prod_config.cors_enabled}")
print()
print("Development Config:")
print(f" App: {dev_config.app_name} v{dev_config.app_version}")
print(f" Server: {dev_config.host}:{dev_config.port}")
print(f" Database: {dev_config.database_url}")
print(f" Debug: {dev_config.debug_mode}")
print(f" Log Level: {dev_config.log_level}")
demonstrate_config_builder()
Example 2: Email Builder with Templates
import java.util.*;
import java.util.regex.Pattern;
public class Email {
private final String from;
private final List<String> to;
private final List<String> cc;
private final List<String> bcc;
private final String subject;
private final String textBody;
private final String htmlBody;
private final List<Attachment> attachments;
private final Priority priority;
private final boolean isHtml;
public enum Priority {
LOW, NORMAL, HIGH
}
public static class Attachment {
private final String filename;
private final String contentType;
private final byte[] data;
public Attachment(String filename, String contentType, byte[] data) {
this.filename = filename;
this.contentType = contentType;
this.data = data.clone();
}
// Getters
public String getFilename() { return filename; }
public String getContentType() { return contentType; }
public byte[] getData() { return data.clone(); }
}
private Email(EmailBuilder builder) {
this.from = builder.from;
this.to = new ArrayList<>(builder.to);
this.cc = new ArrayList<>(builder.cc);
this.bcc = new ArrayList<>(builder.bcc);
this.subject = builder.subject;
this.textBody = builder.textBody;
this.htmlBody = builder.htmlBody;
this.attachments = new ArrayList<>(builder.attachments);
this.priority = builder.priority;
this.isHtml = builder.isHtml;
}
// Getters
public String getFrom() { return from; }
public List<String> getTo() { return new ArrayList<>(to); }
public List<String> getCc() { return new ArrayList<>(cc); }
public List<String> getBcc() { return new ArrayList<>(bcc); }
public String getSubject() { return subject; }
public String getBody() { return isHtml ? htmlBody : textBody; }
public List<Attachment> getAttachments() { return new ArrayList<>(attachments); }
public Priority getPriority() { return priority; }
public boolean isHtml() { return isHtml; }
public static class EmailBuilder {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$");
private String from;
private List<String> to = new ArrayList<>();
private List<String> cc = new ArrayList<>();
private List<String> bcc = new ArrayList<>();
private String subject;
private String textBody;
private String htmlBody;
private List<Attachment> attachments = new ArrayList<>();
private Priority priority = Priority.NORMAL;
private boolean isHtml = false;
public EmailBuilder from(String from) {
if (!isValidEmail(from)) {
throw new IllegalArgumentException("Invalid from email: " + from);
}
this.from = from;
return this;
}
public EmailBuilder to(String... recipients) {
for (String recipient : recipients) {
if (!isValidEmail(recipient)) {
throw new IllegalArgumentException("Invalid to email: " + recipient);
}
this.to.add(recipient);
}
return this;
}
public EmailBuilder cc(String... recipients) {
for (String recipient : recipients) {
if (!isValidEmail(recipient)) {
throw new IllegalArgumentException("Invalid cc email: " + recipient);
}
this.cc.add(recipient);
}
return this;
}
public EmailBuilder bcc(String... recipients) {
for (String recipient : recipients) {
if (!isValidEmail(recipient)) {
throw new IllegalArgumentException("Invalid bcc email: " + recipient);
}
this.bcc.add(recipient);
}
return this;
}
public EmailBuilder subject(String subject) {
this.subject = subject;
return this;
}
public EmailBuilder textBody(String body) {
this.textBody = body;
this.isHtml = false;
return this;
}
public EmailBuilder htmlBody(String body) {
this.htmlBody = body;
this.isHtml = true;
return this;
}
public EmailBuilder attachment(String filename, String contentType, byte[] data) {
this.attachments.add(new Attachment(filename, contentType, data));
return this;
}
public EmailBuilder priority(Priority priority) {
this.priority = priority;
return this;
}
// Template methods for common email types
public EmailBuilder welcomeEmail(String userName, String activationLink) {
return subject("Welcome to Our Service!")
.htmlBody(String.format(
"<h1>Welcome, %s!</h1>" +
"<p>Thank you for joining our service.</p>" +
"<p><a href='%s'>Click here to activate your account</a></p>" +
"<p>Best regards,<br>The Team</p>",
userName, activationLink));
}
public EmailBuilder passwordResetEmail(String userName, String resetLink) {
return subject("Password Reset Request")
.htmlBody(String.format(
"<h1>Password Reset</h1>" +
"<p>Hi %s,</p>" +
"<p>You requested a password reset.</p>" +
"<p><a href='%s'>Click here to reset your password</a></p>" +
"<p>If you didn't request this, please ignore this email.</p>",
userName, resetLink))
.priority(Priority.HIGH);
}
public EmailBuilder notificationEmail(String title, String message) {
return subject("Notification: " + title)
.textBody(message);
}
public Email build() {
if (from == null || from.trim().isEmpty()) {
throw new IllegalStateException("From address is required");
}
if (to.isEmpty()) {
throw new IllegalStateException("At least one recipient is required");
}
if (subject == null || subject.trim().isEmpty()) {
throw new IllegalStateException("Subject is required");
}
if ((textBody == null || textBody.trim().isEmpty()) &&
(htmlBody == null || htmlBody.trim().isEmpty())) {
throw new IllegalStateException("Email body is required");
}
return new Email(this);
}
private boolean isValidEmail(String email) {
return email != null && EMAIL_PATTERN.matcher(email).matches();
}
}
@Override
public String toString() {
return String.format("Email{from='%s', to=%s, subject='%s', priority=%s}",
from, to, subject, priority);
}
}
// Usage examples
public class EmailBuilderDemo {
public static void main(String[] args) {
// Simple email
Email simpleEmail = new Email.EmailBuilder()
.from("sender@example.com")
.to("recipient@example.com")
.subject("Hello World")
.textBody("This is a simple text email.")
.build();
// Complex email with attachments
Email complexEmail = new Email.EmailBuilder()
.from("admin@company.com")
.to("user1@example.com", "user2@example.com")
.cc("manager@company.com")
.bcc("archive@company.com")
.subject("Monthly Report")
.htmlBody("<h1>Monthly Report</h1><p>Please find the attached report.</p>")
.attachment("report.pdf", "application/pdf", new byte[]{1, 2, 3})
.priority(Email.Priority.HIGH)
.build();
// Template-based email
Email welcomeEmail = new Email.EmailBuilder()
.from("noreply@myapp.com")
.to("newuser@example.com")
.welcomeEmail("John Doe", "https://myapp.com/activate?token=xyz123")
.build();
Email passwordResetEmail = new Email.EmailBuilder()
.from("security@myapp.com")
.to("user@example.com")
.passwordResetEmail("Jane Smith", "https://myapp.com/reset?token=abc456")
.build();
System.out.println(simpleEmail);
System.out.println(complexEmail);
System.out.println(welcomeEmail);
System.out.println(passwordResetEmail);
}
}
Debugging Demonstration
Show common errors and how to fix them:
// Common mistake: Forgetting to validate required fields
public class BrokenBuilder {
public Product build() {
// Bug: No validation - could create invalid objects
return new Product(this);
}
}
// Fixed version with proper validation
public class ValidatingBuilder {
public Product build() {
List<String> errors = new ArrayList<>();
if (name == null || name.trim().isEmpty()) {
errors.add("Name is required");
}
if (price < 0) {
errors.add("Price cannot be negative");
}
if (category == null) {
errors.add("Category is required");
}
if (!errors.isEmpty()) {
throw new IllegalStateException("Validation errors: " + String.join(", ", errors));
}
return new Product(this);
}
}
6. Hands-On Practice (30-40 minutes)
Exercise 1: Basic Practice (10 minutes)
Objective: Create a Builder for a Restaurant Order system
Instructions:
- Create an
Orderclass with fields: orderId, customerName, items (List), specialInstructions, deliveryAddress - Implement a fluent OrderBuilder with methods: customer(), addItem(), specialInstructions(), deliveryAddress()
- Add validation to ensure orderId and customerName are required
- Test with different order configurations
Starter Code:
public class Order {
// TODO: Add private final fields
// TODO: Add private constructor that takes OrderBuilder
// TODO: Add getters
public static class OrderBuilder {
// TODO: Add fields for building
// TODO: Add fluent methods
public Order build() {
// TODO: Add validation and return Order
return null;
}
}
}
Exercise 2: Intermediate Challenge (15 minutes)
Objective: Implement a Step Builder for creating API endpoints
Requirements:
- Create a RESTful API endpoint builder with steps: method() → path() → handler() → optional middleware() → build()
- Support HTTP methods: GET, POST, PUT, DELETE
- Add validation for path format (must start with /)
- Include optional authentication and rate limiting middleware
Challenge Features:
- Add support for path parameters (e.g., /users/{id})
- Implement request/response content type specification
- Create middleware chaining functionality
Exercise 3: Creative Application (15 minutes)
Objective: Design a Game Character Builder
Challenge: Create a character builder for an RPG game that:
- Has required fields: name, characterClass (Warrior, Mage, Archer)
- Has optional fields: level, hitPoints, skills, equipment, appearance
- Supports different character creation flows (quick create vs custom)
- Validates character constraints (e.g., Mages can’t use heavy armor)
Extension Ideas:
- Add character templates for different play styles
- Implement skill point allocation system
- Create equipment compatibility checking
- Add character export/import functionality
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
-
Problem: Mutable builder objects causing shared state issues Cause: Reusing builder instances without resetting state Solution: Create new builder instances or implement reset() method
-
Problem: Missing validation in build() method Cause: Assuming all required fields will be set by client code Solution: Always validate required fields and object constraints in build()
-
Problem: Not making built objects immutable Cause: Exposing mutable fields or collections in the product Solution: Use final fields and defensive copying for collections
-
Problem: Over-complicated builders for simple objects Cause: Using Builder pattern when simple constructors would suffice Solution: Only use Builder when you have optional parameters or complex construction logic
Debugging Tips
- Add logging to track builder method calls and validation steps
- Use unit tests to verify builder validation and immutability
- Test edge cases like null values and invalid parameter combinations
- Monitor builder usage patterns to identify unnecessary complexity
When NOT to Use Builder Pattern
- Objects with only a few required parameters
- Objects that change frequently after creation
- Performance-critical code where object creation overhead matters
- Simple value objects without complex construction logic
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Make product objects immutable whenever possible for thread safety
- Validate in build() method to ensure object consistency
- Use descriptive method names that clearly indicate what they set
- Consider Step Builder for complex objects with required ordering
Next Steps in Your Learning Journey
- Study Prototype pattern for object cloning and copying
- Learn Abstract Factory for creating families of related objects
- Explore Fluent Interface design for more readable APIs
- Investigate Immutable objects and their benefits in concurrent programming
Modern Framework Examples
- Lombok @Builder - Generates builder code automatically in Java
- JavaScript libraries - Many use builder patterns for configuration (axios, lodash)
- Spring Framework - Uses builders for test configurations and web security
- Android - AlertDialog.Builder, Notification.Builder
9. Summary and Review (5 minutes)
Key Takeaways
- Builder pattern makes complex object creation intuitive through fluent interfaces
- Immutability and validation are key benefits of well-designed builders
- Choose appropriate builder type based on complexity: Simple Builder → Fluent Builder → Step Builder
- Balance flexibility with simplicity - don’t over-engineer simple object creation
Quick Review Questions
- What are the main advantages of Builder pattern over telescoping constructors?
- When would you choose Step Builder over regular fluent Builder?
- How does Builder pattern support immutability and thread safety?
For Instructors
Teaching Tips
- Start with the problems Builder solves (telescoping constructors) before showing solutions
- Use live coding to demonstrate the evolution from simple to complex builders
- Emphasize validation and immutability as key design considerations
- Show real-world examples from popular frameworks and libraries
Assessment Ideas
- Refactoring exercise: Convert telescoping constructor anti-pattern to Builder
- Design challenge: Choose appropriate Builder variant for given requirements
- Code review: Identify and fix issues in Builder implementations
Adaptation Notes
- Beginners: Focus on basic fluent builders with simple validation
- Advanced: Discuss performance implications, generic builders, and integration with frameworks
- Language-specific: Show language features like Python dataclasses or JavaScript object destructuring