Learning Objectives

  • Understand the Builder pattern concept
  • Implement fluent interfaces
  • Build complex objects step by step
  • Use method chaining effectively
  • Apply builder best practices

What is the Builder Pattern?

Builder allows you to construct complex objects step by step using method chaining for a clean, readable API. Instead of passing many parameters to a constructor, you build the object incrementally.

Basic Implementation

class User {
    constructor(builder) {
        this.name = builder.name;
        this.email = builder.email;
        this.age = builder.age;
        this.address = builder.address;
        this.phone = builder.phone;
    }
}

class UserBuilder {
    constructor(name) {
        this.name = name;
    }
    
    setEmail(email) {
        this.email = email;
        return this; // Return this for chaining
    }
    
    setAge(age) {
        this.age = age;
        return this;
    }
    
    setAddress(address) {
        this.address = address;
        return this;
    }
    
    setPhone(phone) {
        this.phone = phone;
        return this;
    }
    
    build() {
        return new User(this);
    }
}

// Usage - clean and readable
const user = new UserBuilder('John Doe')
    .setEmail('john@example.com')
    .setAge(30)
    .setAddress('123 Main St')
    .setPhone('555-1234')
    .build();

console.log(user);
// User { name: 'John Doe', email: 'john@example.com', age: 30, ... }

Fluent Interface

class QueryBuilder {
    constructor() {
        this.query = '';
        this.params = [];
    }
    
    select(...fields) {
        this.query = `SELECT ${fields.join(', ')}`;
        return this;
    }
    
    from(table) {
        this.query += ` FROM ${table}`;
        return this;
    }
    
    where(condition, value) {
        this.query += ` WHERE ${condition}`;
        this.params.push(value);
        return this;
    }
    
    and(condition, value) {
        this.query += ` AND ${condition}`;
        this.params.push(value);
        return this;
    }
    
    orderBy(field, direction = 'ASC') {
        this.query += ` ORDER BY ${field} ${direction}`;
        return this;
    }
    
    limit(count) {
        this.query += ` LIMIT ${count}`;
        return this;
    }
    
    build() {
        return { 
            query: this.query, 
            params: this.params 
        };
    }
}

// Usage
const query = new QueryBuilder()
    .select('id', 'name', 'email')
    .from('users')
    .where('age > ?', 18)
    .and('status = ?', 'active')
    .orderBy('name')
    .limit(10)
    .build();

console.log(query);
// { query: 'SELECT id, name, email FROM users WHERE age > ? AND status = ? ORDER BY name LIMIT 10',
//   params: [18, 'active'] }

Real-World Examples

HTTP Request Builder

class RequestBuilder {
    constructor(url) {
        this.url = url;
        this.method = 'GET';
        this.headers = {};
        this.body = null;
        this.timeout = 5000;
    }
    
    setMethod(method) {
        this.method = method.toUpperCase();
        return this;
    }
    
    setHeader(key, value) {
        this.headers[key] = value;
        return this;
    }
    
    setHeaders(headers) {
        this.headers = { ...this.headers, ...headers };
        return this;
    }
    
    setBody(body) {
        this.body = body;
        return this;
    }
    
    setTimeout(timeout) {
        this.timeout = timeout;
        return this;
    }
    
    json() {
        this.setHeader('Content-Type', 'application/json');
        return this;
    }
    
    auth(token) {
        this.setHeader('Authorization', `Bearer ${token}`);
        return this;
    }
    
    async execute() {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), this.timeout);
        
        try {
            const response = await fetch(this.url, {
                method: this.method,
                headers: this.headers,
                body: this.body ? JSON.stringify(this.body) : null,
                signal: controller.signal
            });
            
            clearTimeout(timeoutId);
            return response;
        } catch (error) {
            clearTimeout(timeoutId);
            throw error;
        }
    }
}

// Usage
const response = await new RequestBuilder('/api/users')
    .setMethod('POST')
    .json()
    .auth('my-token-123')
    .setBody({ name: 'John', email: 'john@example.com' })
    .setTimeout(10000)
    .execute();

const data = await response.json();

Email Builder

class EmailBuilder {
    constructor() {
        this.to = [];
        this.cc = [];
        this.bcc = [];
        this.subject = '';
        this.body = '';
        this.attachments = [];
    }
    
    addTo(email) {
        this.to.push(email);
        return this;
    }
    
    addCc(email) {
        this.cc.push(email);
        return this;
    }
    
    addBcc(email) {
        this.bcc.push(email);
        return this;
    }
    
    setSubject(subject) {
        this.subject = subject;
        return this;
    }
    
    setBody(body) {
        this.body = body;
        return this;
    }
    
    addAttachment(file) {
        this.attachments.push(file);
        return this;
    }
    
    build() {
        if (this.to.length === 0) {
            throw new Error('Email must have at least one recipient');
        }
        if (!this.subject) {
            throw new Error('Email must have a subject');
        }
        
        return {
            to: this.to,
            cc: this.cc,
            bcc: this.bcc,
            subject: this.subject,
            body: this.body,
            attachments: this.attachments
        };
    }
    
    async send() {
        const email = this.build();
        // Send email logic here
        console.log('Sending email:', email);
        return true;
    }
}

// Usage
await new EmailBuilder()
    .addTo('user1@example.com')
    .addTo('user2@example.com')
    .addCc('manager@example.com')
    .setSubject('Meeting Tomorrow')
    .setBody('Don\'t forget about our meeting at 2pm')
    .addAttachment('agenda.pdf')
    .send();

When to Use Builder Pattern

Good use cases:
  • Complex object construction with many parameters
  • Many optional parameters
  • Immutable objects
  • Fluent APIs and DSLs
  • Query builders
  • Configuration objects

Benefits

Best Practices

1. Always return this for chaining
setName(name) {
    this.name = name;
    return this; // Essential for chaining
}
2. Validate in build() method
build() {
    if (!this.name) {
        throw new Error('Name is required');
    }
    return new User(this);
}
3. Use descriptive method names
// Good
.setEmail('test@example.com')
.withTimeout(5000)
.addHeader('Content-Type', 'application/json')

// Not as clear
.email('test@example.com')
.timeout(5000)
.header('Content-Type', 'application/json')

Key Takeaways