Skip to content

Constructing complex objects with Builder Pattern

When an object has many properties, passing a long list of parameters to a constructor can become messy and hard to read. This is often known as the “telescoping constructor” anti-pattern.

The Builder Pattern is a creational design pattern that solves this by letting you construct complex objects step by step, improving readability and maintainability.

Instead of initializing directly with a massive constructor:

// Hard to remember what each parameter means!
const user = new User("John", "Doe", 30, "john@example.com", "123 Main St", true);

Using the Builder Pattern:

class User {
constructor(
public firstName: string,
public lastName: string,
public age?: number,
public email?: string,
public address?: string,
public isActive?: boolean
) {}
}
class UserBuilder {
private user: Partial<User> = {};
// Required fields can be set in the Builder's constructor
constructor(firstName: string, lastName: string) {
this.user.firstName = firstName;
this.user.lastName = lastName;
}
setAge(age: number): this {
this.user.age = age;
return this; // Return 'this' to allow method chaining
}
setEmail(email: string): this {
this.user.email = email;
return this;
}
setAddress(address: string): this {
this.user.address = address;
return this;
}
setIsActive(isActive: boolean): this {
this.user.isActive = isActive;
return this;
}
build(): User {
// Construct the final object
return new User(
this.user.firstName!,
this.user.lastName!,
this.user.age,
this.user.email,
this.user.address,
this.user.isActive
);
}
}
// Construction is fluent, explicit, and easy to read
const user = new UserBuilder("John", "Doe")
.setAge(30)
.setEmail("john@example.com")
.setAddress("123 Main St")
.setIsActive(true)
.build();

This pattern makes your code more readable, flexible, and provides a clear separation between object construction and its representation.

Real-world Examples

The Builder pattern is extremely common in the TypeScript/JavaScript ecosystem, especially for constructing complex queries or configuration objects. Here are a few popular examples:

  1. Knex.js & TypeORM: SQL query builders heavily rely on this pattern to construct complex database queries step by step.

    // TypeORM example
    const user = await dataSource
    .createQueryBuilder()
    .select("user")
    .from(User, "user")
    .where("user.id = :id", { id: 1 })
    .getOne();
  2. Supertest (with Jest): Used for HTTP assertions, where you build a request step by step.

    await request(app)
    .get('/user')
    .set('Accept', 'application/json')
    .expect('Content-Type', /json/)
    .expect(200);
  3. Mongoose: Querying MongoDB uses a chained builder approach.

    User.find()
    .where('age').gt(18)
    .sort('-createdAt')
    .limit(10)
    .exec();