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
- Readable code: Method chaining is self-documenting
- Flexible construction: Build objects in any order
- Immutability support: Build once, use forever
- Method chaining: Clean, fluent API
- Clear API: Easy to understand and use
- Validation: Validate in build() method
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
- Builder constructs complex objects step by step
- Method chaining creates fluent interfaces
- Perfect for objects with many optional parameters
- Always return
thisfor chaining - Call
build()to create final object - Validate in build() method
- Makes code more readable and maintainable
- Great for immutable objects