Skip to main content
  1. Examples/

The Builder Pattern - Building Complex Objects Step by Step

·17 mins· loading · loading · · ·
Design Back-End
Adrien D'acunto
Author
Adrien D’acunto
Table of Contents

The Builder Pattern: Building Complex Objects Step by Step
#

Introduction
#

The Builder pattern is a creational design pattern that allows you to build complex objects step by step. It separates the construction of an object from its representation, thus allowing different representations to be created with the same construction process. In this article, we will explore this pattern through a concrete example: building a student cohort with data validation.

What is the Builder?
#

Definition
#

The Builder pattern allows you to:

  • Build complex objects step by step
  • Produce different types of objects with the same process
  • Isolate construction code from business logic
  • Validate data before creating the final object

UML Structure
#

classDiagram
    class BuilderInterface~T~ {
        <<interface>>
        +build() T
    }
    
    class ConcreteBuilder {
        -field1
        -field2
        +method1() ConcreteBuilder
        +method2() ConcreteBuilder
        +build() Product
    }
    
    class Product {
        +property1
        +property2
    }
    
    BuilderInterface <|.. ConcreteBuilder : implements
    ConcreteBuilder ..> Product : creates

Key Characteristics
#

Characteristic Description
Fluent Interface Method chaining
Validation Data verification before construction
Immutability Final object can be immutable
Separation Construction separated from representation

Problem without Builder
#

The Telescoping Constructor Problem
#

// Without Builder: Multiple constructors (anti-pattern)
class Promo {
    constructor(name: string) { /* ... */ }
}

class Promo {
    constructor(name: string, beginAt: Date) { /* ... */ }
}

class Promo {
    constructor(name: string, beginAt: Date, endAt: Date) { /* ... */ }
}

// Confusing usage
const promo1 = new Promo("Dev 2025");
const promo2 = new Promo("Dev 2025", new Date());
const promo3 = new Promo("Dev 2025", new Date(), new Date());

The Problem of Partially Constructed Objects
#

// Without Builder: Object temporarily invalid
const promo = new Promo();
// Promo exists but is not valid
promo.setName("Dev 2025");
// Still not valid
promo.setBeginAt(new Date());
// Finally valid
promo.setEndAt(new Date());

Consequences:

  • Objects in an invalid state during construction
  • No centralized validation
  • Verbose and repetitive code
  • Difficult to maintain consistency

TypeScript Implementation
#

Complete Architecture
#

classDiagram
    class BuilderInterface~T~ {
        <<interface>>
        +build() T
    }
    
    class PromoBuilder {
        -_name: string
        -_beginAt: Date
        -_endAt: Date
        +name(name: string) PromoBuilder
        +beginAt(date: Date) PromoBuilder
        +endAt(date: Date) PromoBuilder
        +build() Promo
    }
    
    class Promo {
        -name: string
        -beginAt: Date
        -endAt: Date
        -students: CollectionInterface~Student~
        +getName() string
        +setName(name: string) void
        +getBeginAt() Date
        +setBeginAt(beginAt: Date) void
        +getEndAt() Date
        +setEndAt(endAt: Date) void
        +addStudent(student: Student) void
        +removeStudent(student: Student) void
        +getStudentsNumber() number
    }
    
    class CollectionInterface~T~ {
        <<interface>>
        +add(item: T) void
        +remove(item: T) void
        +getLength() number
    }
    
    class StudentCollection {
        +add(student: Student) void
        +remove(student: Student) void
        +getLength() number
    }
    
    BuilderInterface <|.. PromoBuilder : implements
    PromoBuilder ..> Promo : creates
    Promo --> CollectionInterface : uses
    CollectionInterface <|.. StudentCollection : implements

Generic Builder Interface
#

export interface BuilderInterface<T> {
    build(): T
}

This generic interface defines the base contract:

  • Type parameter T: the type of object to build
  • Method build(): returns the final constructed instance

Promo Class
#

import type { CollectionInterface } from "../core/collection/collection-interface";
import type { Student } from "./student";
import { StudentCollection } from "./student-collection";

export class Promo {
    private name: string = ''
    private beginAt: Date = new Date()
    private endAt: Date = new Date()
    private students: CollectionInterface<Student> = new StudentCollection()

    public getName(): string {
        return this.name;
    }

    public setName(name: string): void {
        if (this.name === '')
            this.name = name;
    }

    public getBeginAt(): Date {
        return this.beginAt;
    }

    public setBeginAt(beginAt: Date): void {
        this.beginAt = beginAt;
    }

    public getEndAt(): Date {
        return this.endAt;
    }

    public setEndAt(endAt: Date): void {
        this.endAt = endAt;
    }

    public addStudent(student: Student): void {
        this.students.add(student)
    }

    public removeStudent(student: Student): void {
        this.students.remove(student)
    }

    public getStudentsNumber(): number {
        return this.students.getLength()
    }
}

Key points:

  • Private attributes with default values
  • Protection against name modification (partial immutability)
  • Management of a student collection
  • Simple and clear interface

PromoBuilder
#

import type { BuilderInterface } from "../core/builder/builder.interface";
import { Promo } from "./promo";

export class PromoBuilder implements BuilderInterface<Promo> {
    private _name: string = ''
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date()
    
    public name(name: string): PromoBuilder {
        this._name = name
        return this
    }

    public beginAt(date: Date): PromoBuilder {
        this._beginAt = date
        return this
    }

    public endAt(date: Date): PromoBuilder {
        this._endAt = date
        return this
    }

    build(): Promo {
        if (this._name === '') {
            throw new Error('Promo name is required')
        }

        if (this._beginAt > this._endAt) {
            throw new Error("Incorrect dates");
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)

        return promo
    }
}

Builder characteristics:

  • Private attributes prefixed with _ (convention)
  • Each method returns this (fluent interface)
  • Validation in build() before creation
  • Clear separation of construction/validation

Code Analysis
#

Sequence Diagram: Building a Promo
#

sequenceDiagram
    participant Client
    participant Builder as PromoBuilder
    participant Promo
    
    Client->>Builder: new PromoBuilder()
    Client->>Builder: name("Dev 2025")
    Builder-->>Client: this
    
    Client->>Builder: beginAt(2025-01-15)
    Builder-->>Client: this
    
    Client->>Builder: endAt(2025-12-20)
    Builder-->>Client: this
    
    Client->>Builder: build()
    
    alt Validation OK
        Builder->>Builder: Check name
        Builder->>Builder: Check dates
        Builder->>Promo: new Promo()
        Builder->>Promo: setName()
        Builder->>Promo: setBeginAt()
        Builder->>Promo: setEndAt()
        Promo-->>Builder: promo
        Builder-->>Client: promo
    else Validation Failed
        Builder-->>Client: throw Error
    end

Fluent Interface: Method Chaining
#

public name(name: string): PromoBuilder {
    this._name = name
    return this  // Returns current instance
}

This pattern enables chaining:

const promo = new PromoBuilder()
    .name("Dev 2025")
    .beginAt(new Date("2025-01-15"))
    .endAt(new Date("2025-12-20"))
    .build();

Advantages:

  • Readable and expressive code
  • Flexible order of calls
  • Construction in single expression

Centralized Validation
#

build(): Promo {
    // Validation 1: Name required
    if (this._name === '') {
        throw new Error('Promo name is required')
    }

    // Validation 2: Coherent dates
    if (this._beginAt > this._endAt) {
        throw new Error("Incorrect dates");
    }
    
    // Construction only if validations OK
    const promo = new Promo()
    promo.setName(this._name)
    promo.setBeginAt(this._beginAt)
    promo.setEndAt(this._endAt)

    return promo
}

Benefits:

  • All validations in one place
  • Final object always valid
  • Explicit errors before creation

Separation of Responsibilities
#

Class Responsibility
Promo Represent a cohort (business logic)
PromoBuilder Build and validate a cohort
BuilderInterface Define construction contract

Practical Usage
#

Complete Example
#

// main.ts
import { PromoBuilder } from './promo-builder';
import { Student } from './student';

// Simple construction
const promo = new PromoBuilder()
    .name("Web Development 2025")
    .beginAt(new Date("2025-01-15"))
    .endAt(new Date("2025-12-20"))
    .build();

console.log(promo.getName()); // "Web Development 2025"
console.log(promo.getStudentsNumber()); // 0

// Adding students
const alice = new Student("Alice", "Martin");
const bob = new Student("Bob", "Dupont");

promo.addStudent(alice);
promo.addStudent(bob);

console.log(promo.getStudentsNumber()); // 2

Error Handling
#

// Error: Missing name
try {
    const promo = new PromoBuilder()
        .beginAt(new Date("2025-01-15"))
        .endAt(new Date("2025-12-20"))
        .build();
} catch (error) {
    console.error(error.message); // "Promo name is required"
}

// Error: Incoherent dates
try {
    const promo = new PromoBuilder()
        .name("Dev 2025")
        .beginAt(new Date("2025-12-20"))
        .endAt(new Date("2025-01-15"))
        .build();
} catch (error) {
    console.error(error.message); // "Incorrect dates"
}

Unit Tests
#

// promo-builder.test.ts
import { describe, it, expect } from 'vitest';
import { PromoBuilder } from './promo-builder';

describe('PromoBuilder', () => {
    
    it('should build a valid promo', () => {
        const promo = new PromoBuilder()
            .name("Dev 2025")
            .beginAt(new Date("2025-01-15"))
            .endAt(new Date("2025-12-20"))
            .build();
        
        expect(promo.getName()).toBe("Dev 2025");
        expect(promo.getBeginAt()).toEqual(new Date("2025-01-15"));
        expect(promo.getEndAt()).toEqual(new Date("2025-12-20"));
    });
    
    it('should reject empty name', () => {
        expect(() => {
            new PromoBuilder()
                .beginAt(new Date("2025-01-15"))
                .endAt(new Date("2025-12-20"))
                .build();
        }).toThrow("Promo name is required");
    });
    
    it('should reject incoherent dates', () => {
        expect(() => {
            new PromoBuilder()
                .name("Dev 2025")
                .beginAt(new Date("2025-12-20"))
                .endAt(new Date("2025-01-15"))
                .build();
        }).toThrow("Incorrect dates");
    });
    
    it('should allow method chaining', () => {
        const builder = new PromoBuilder();
        
        const result1 = builder.name("Dev 2025");
        const result2 = result1.beginAt(new Date());
        
        expect(result1).toBe(builder);
        expect(result2).toBe(builder);
    });
});

Advantages and Disadvantages
#

Advantages
#

Advantage Description Example
Readable code Fluent and expressive syntax .name("Dev").beginAt(date)
Centralized validation All checks in one place build() method
Atomic construction Error before creation -
Flexibility Parameter order is free Start with any setter
Reusability Builder is reusable Create multiple similar objects
Possible immutability Final object can be immutable No public setters

Disadvantages
#

Disadvantage Description Impact
Verbose code Additional builder class More files
Duplication Repeated attributes (class + builder) Increased maintenance
Complexity Heavy pattern for simple objects Unnecessary overhead
Memory Temporary builder instance Garbage collection

When to Use the Builder?
#

Use the Builder for:

// Object with many parameters
class Configuration {
    // 15+ parameters
}
const config = new ConfigurationBuilder()
    .host("localhost")
    .port(3000)
    .timeout(5000)
    .retries(3)
    // ...
    .build();

// Objects with complex validations
class User {
    // Multiple interdependent validations
}
const user = new UserBuilder()
    .email("test@example.com")
    .password("secure123")
    .confirmPassword("secure123")
    .age(25)
    .build();

// Creating immutable objects
class Invoice {
    // Readonly after construction
}
const invoice = new InvoiceBuilder()
    .customer(customer)
    .items(items)
    .build(); // Immutable object

Do NOT use the Builder for:

// Simple objects
class Point {
    constructor(public x: number, public y: number) {}
}
// new Point(10, 20) is enough

// Objects with few parameters
class Color {
    constructor(public r: number, public g: number, public b: number) {}
}
// No need for ColorBuilder

// Objects without validation
class Label {
    constructor(public text: string) {}
}
// Too simple for a builder

Pattern Variants
#

Builder with Default Values
#

export class PromoBuilderWithDefaults implements BuilderInterface<Promo> {
    private _name: string = 'Unnamed Promo'
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // +1 year
    
    public name(name: string): PromoBuilderWithDefaults {
        this._name = name
        return this
    }

    public beginAt(date: Date): PromoBuilderWithDefaults {
        this._beginAt = date
        return this
    }

    public endAt(date: Date): PromoBuilderWithDefaults {
        this._endAt = date
        return this
    }

    build(): Promo {
        // No error if name missing (default value)
        if (this._beginAt > this._endAt) {
            throw new Error("Incorrect dates");
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)

        return promo
    }
}

// Usage
const promo = new PromoBuilderWithDefaults().build(); // Uses defaults

Builder with Mandatory Steps (Step Builder)
#

// Step 1: Name mandatory
interface INameStep {
    name(name: string): IBeginDateStep;
}

// Step 2: Begin date mandatory
interface IBeginDateStep {
    beginAt(date: Date): IEndDateStep;
}

// Step 3: End date mandatory
interface IEndDateStep {
    endAt(date: Date): IBuildStep;
}

// Step 4: Final construction
interface IBuildStep {
    build(): Promo;
}

export class StepPromoBuilder implements INameStep, IBeginDateStep, IEndDateStep, IBuildStep {
    private _name: string = ''
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date()
    
    public name(name: string): IBeginDateStep {
        this._name = name
        return this
    }

    public beginAt(date: Date): IEndDateStep {
        this._beginAt = date
        return this
    }

    public endAt(date: Date): IBuildStep {
        this._endAt = date
        return this
    }

    build(): Promo {
        if (this._beginAt > this._endAt) {
            throw new Error("Incorrect dates");
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)

        return promo
    }
}

// Usage: Order forced by TypeScript
const promo = new StepPromoBuilder()
    .name("Dev 2025")        // Must be called first
    .beginAt(new Date())     // Then this
    .endAt(new Date())       // Then this
    .build();                // Finally build()

Builder with Reset
#

export class ReusablePromoBuilder implements BuilderInterface<Promo> {
    private _name: string = ''
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date()
    
    public name(name: string): ReusablePromoBuilder {
        this._name = name
        return this
    }

    public beginAt(date: Date): ReusablePromoBuilder {
        this._beginAt = date
        return this
    }

    public endAt(date: Date): ReusablePromoBuilder {
        this._endAt = date
        return this
    }

    // Method to reset the builder
    public reset(): ReusablePromoBuilder {
        this._name = ''
        this._beginAt = new Date()
        this._endAt = new Date()
        return this
    }

    build(): Promo {
        if (this._name === '') {
            throw new Error('Promo name is required')
        }

        if (this._beginAt > this._endAt) {
            throw new Error("Incorrect dates");
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)

        return promo
    }
}

// Usage: Builder reuse
const builder = new ReusablePromoBuilder();

const promo1 = builder
    .name("Promo A")
    .beginAt(new Date("2025-01-01"))
    .endAt(new Date("2025-06-30"))
    .build();

const promo2 = builder
    .reset() // Reset
    .name("Promo B")
    .beginAt(new Date("2025-07-01"))
    .endAt(new Date("2025-12-31"))
    .build();

Builder with Director (Complete Pattern)
#

// Director: encapsulates predefined constructions
export class PromoDirector {
    constructor(private builder: PromoBuilder) {}
    
    // Standard construction
    public buildStandardPromo(name: string, year: number): Promo {
        return this.builder
            .name(name)
            .beginAt(new Date(`${year}-01-15`))
            .endAt(new Date(`${year}-12-20`))
            .build();
    }
    
    // Summer internship construction
    public buildSummerPromo(name: string, year: number): Promo {
        return this.builder
            .name(`${name} - Summer`)
            .beginAt(new Date(`${year}-06-01`))
            .endAt(new Date(`${year}-08-31`))
            .build();
    }
    
    // Intensive construction (bootcamp)
    public buildBootcamp(name: string): Promo {
        const start = new Date();
        const end = new Date();
        end.setDate(end.getDate() + 90); // 3 months
        
        return this.builder
            .name(`Bootcamp ${name}`)
            .beginAt(start)
            .endAt(end)
            .build();
    }
}

// Usage
const builder = new PromoBuilder();
const director = new PromoDirector(builder);

const promo1 = director.buildStandardPromo("Web Dev", 2025);
const promo2 = director.buildSummerPromo("Mobile Dev", 2025);
const promo3 = director.buildBootcamp("Full Stack");

Best Practices
#

Consistent Naming
#

// GOOD: Clear and consistent names
class PromoBuilder {
    name(name: string): PromoBuilder { /* ... */ }
    beginAt(date: Date): PromoBuilder { /* ... */ }
    endAt(date: Date): PromoBuilder { /* ... */ }
}

// BAD: Inconsistent names
class PromoBuilder {
    setName(name: string): PromoBuilder { /* ... */ }
    withBeginDate(date: Date): PromoBuilder { /* ... */ }
    endingAt(date: Date): PromoBuilder { /* ... */ }
}

Complete Validation
#

build(): Promo {
    // All validations in one block
    const errors: string[] = [];
    
    if (this._name === '') {
        errors.push('Promo name is required');
    }
    
    if (this._name.length < 3) {
        errors.push('Promo name must be at least 3 characters');
    }
    
    if (this._beginAt > this._endAt) {
        errors.push('Begin date must be before end date');
    }
    
    if (this._endAt < new Date()) {
        errors.push('End date must be in the future');
    }
    
    // Throw all errors at once
    if (errors.length > 0) {
        throw new Error(`Validation failed:\n- ${errors.join('\n- ')}`);
    }
    
    // Construction
    const promo = new Promo()
    promo.setName(this._name)
    promo.setBeginAt(this._beginAt)
    promo.setEndAt(this._endAt)

    return promo
}

Clear Documentation
#

/**
 * Builder for creating student cohorts
 * 
 * @example
 * ```typescript
 * const promo = new PromoBuilder()
 *     .name("Web Dev 2025")
 *     .beginAt(new Date("2025-01-15"))
 *     .endAt(new Date("2025-12-20"))
 *     .build();
 * ```
 * 
 * @throws {Error} If name is empty
 * @throws {Error} If dates are incoherent
 */
export class PromoBuilder implements BuilderInterface<Promo> {
    
    /**
     * Sets the cohort name
     * 
     * @param name - Cohort name (minimum 3 characters)
     * @returns The builder instance for chaining
     */
    public name(name: string): PromoBuilder {
        this._name = name
        return this
    }
    
    // ...
}

Explicit Return Types
#

// GOOD: Explicit return type
public name(name: string): PromoBuilder {
    this._name = name
    return this
}

// BAD: Inferred type (less clear)
public name(name: string) {
    this._name = name
    return this
}

Optional Methods
#

export class PromoBuilder implements BuilderInterface<Promo> {
    private _name: string = ''
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date()
    private _maxStudents?: number // Optional
    private _description?: string // Optional
    
    // Mandatory methods
    public name(name: string): PromoBuilder {
        this._name = name
        return this
    }
    
    // Optional methods
    public maxStudents(max: number): PromoBuilder {
        this._maxStudents = max
        return this
    }
    
    public description(desc: string): PromoBuilder {
        this._description = desc
        return this
    }

    build(): Promo {
        // Validations only on mandatory fields
        if (this._name === '') {
            throw new Error('Promo name is required')
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)
        
        // Optional fields
        if (this._maxStudents !== undefined) {
            promo.setMaxStudents(this._maxStudents)
        }
        
        if (this._description !== undefined) {
            promo.setDescription(this._description)
        }

        return promo
    }
}

Advanced Builder with TypeScript
#

Generic Reusable Builder
#

// Generic builder for any class
export abstract class GenericBuilder<T> implements BuilderInterface<T> {
    protected properties: Partial<T> = {}
    
    protected set<K extends keyof T>(key: K, value: T[K]): this {
        this.properties[key] = value
        return this
    }
    
    protected validate(): void {
        // Override in subclasses
    }
    
    abstract build(): T
}

// Specific usage
class PromoGenericBuilder extends GenericBuilder<Promo> {
    name(name: string): this {
        return this.set('name' as any, name)
    }
    
    beginAt(date: Date): this {
        return this.set('beginAt' as any, date)
    }
    
    endAt(date: Date): this {
        return this.set('endAt' as any, date)
    }
    
    protected validate(): void {
        if (!this.properties.name) {
            throw new Error('Name is required')
        }
    }
    
    build(): Promo {
        this.validate()
        
        const promo = new Promo()
        promo.setName(this.properties.name as string)
        promo.setBeginAt(this.properties.beginAt as Date)
        promo.setEndAt(this.properties.endAt as Date)
        
        return promo
    }
}

Builder with Progressive Validation
#

class ValidatingPromoBuilder {
    private _name: string = ''
    private _beginAt: Date = new Date()
    private _endAt: Date = new Date()
    private errors: Map<string, string> = new Map()
    
    name(name: string): ValidatingPromoBuilder {
        this._name = name
        
        // Immediate validation
        if (name.length < 3) {
            this.errors.set('name', 'Name must be at least 3 characters')
        } else {
            this.errors.delete('name')
        }
        
        return this
    }
    
    beginAt(date: Date): ValidatingPromoBuilder {
        this._beginAt = date
        
        // Coherence validation with endAt
        if (this._endAt && date > this._endAt) {
            this.errors.set('dates', 'Begin date must be before end date')
        } else {
            this.errors.delete('dates')
        }
        
        return this
    }
    
    endAt(date: Date): ValidatingPromoBuilder {
        this._endAt = date
        
        // Coherence validation with beginAt
        if (this._beginAt && this._beginAt > date) {
            this.errors.set('dates', 'End date must be after begin date')
        } else {
            this.errors.delete('dates')
        }
        
        return this
    }
    
    // Check errors anytime
    hasErrors(): boolean {
        return this.errors.size > 0
    }
    
    getErrors(): string[] {
        return Array.from(this.errors.values())
    }
    
    build(): Promo {
        if (this.hasErrors()) {
            throw new Error(`Validation errors:\n- ${this.getErrors().join('\n- ')}`)
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        promo.setBeginAt(this._beginAt)
        promo.setEndAt(this._endAt)
        
        return promo
    }
}

// Usage with progressive feedback
const builder = new ValidatingPromoBuilder()
    .name('Ab') // Too short
    .beginAt(new Date('2025-12-31'))
    .endAt(new Date('2025-01-01')); // Incoherent

console.log(builder.hasErrors()); // true
console.log(builder.getErrors()); 
// ['Name must be at least 3 characters', 'End date must be after begin date']

Anti-Patterns to Avoid
#

Builder Too Complex
#

// BAD: Builder doing too much
class GodPromoBuilder {
    private promo: Promo
    
    constructor() {
        this.promo = new Promo()
    }
    
    name(name: string): this {
        this.promo.setName(name)
        // Business logic in builder (BAD)
        this.notifyAdmins(`New promo: ${name}`)
        this.createDatabaseEntry()
        this.generateReports()
        return this
    }
    
    // Too many responsibilities
    private notifyAdmins(message: string): void { /* ... */ }
    private createDatabaseEntry(): void { /* ... */ }
    private generateReports(): void { /* ... */ }
    
    build(): Promo {
        return this.promo
    }
}

// GOOD: Simple builder, logic separate
class SimplePromoBuilder {
    private _name: string = ''
    
    name(name: string): this {
        this._name = name
        return this
    }
    
    build(): Promo {
        const promo = new Promo()
        promo.setName(this._name)
        return promo
    }
}

// Business logic elsewhere
class PromoService {
    create(promo: Promo): void {
        this.notifyAdmins(promo)
        this.saveToDB(promo)
        this.generateReports(promo)
    }
}

Object Modification After Construction
#

// BAD: Builder that modifies constructed object
class MutablePromoBuilder {
    private promo: Promo = new Promo()
    
    name(name: string): this {
        this.promo.setName(name)
        return this
    }
    
    build(): Promo {
        // Returns direct reference (BAD)
        return this.promo
    }
}

const builder = new MutablePromoBuilder()
const promo1 = builder.name('Promo A').build()
const promo2 = builder.name('Promo B').build() // Also modifies promo 1 ! 

console.log(promo1.getName()) // "Promo B" (PROBLEM)

// GOOD: Create new instance for each build() 
class ImmutablePromoBuilder {
    private _name: string = ''
    
    name(name: string): this {
        this._name = name
        return this
    }
    
    build(): Promo {
        // New instance each time
        const promo = new Promo()
        promo.setName(this._name)
        return promo
    }
}

Insufficient Validation
#

// BAD: No validation
class UnsafePromoBuilder {
    private _name: string = ''
    
    name(name: string): this {
        this._name = name
        return this
    }
    
    build(): Promo {
        // No validation (BAD)
        const promo = new Promo()
        promo.setName(this._name)
        return promo
    }
}

// Allows creating invalid objects
const promo = new UnsafePromoBuilder().build() // Empty name!

// GOOD: Complete validation
class SafePromoBuilder {
    private _name: string = ''
    
    name(name: string): this {
        this._name = name
        return this
    }
    
    build(): Promo {
        if (this._name === '') {
            throw new Error('Name is required')
        }
        
        if (this._name.length < 3) {
            throw new Error('Name must be at least 3 characters')
        }
        
        const promo = new Promo()
        promo.setName(this._name)
        return promo
    }
}

Advanced Tests
#

Validation Tests
#

describe('PromoBuilder - Validation', () => {
    
    it('should validate minimum name length', () => {
        expect(() => {
            new PromoBuilder()
                .name('AB') // Too short
                .beginAt(new Date())
                .endAt(new Date())
                .build()
        }).toThrow('at least 3 characters')
    })
    
    it('should validate future dates', () => {
        const pastDate = new Date('2020-01-01')
        
        expect(() => {
            new PromoBuilder()
                .name('Dev 2025')
                .beginAt(pastDate)
                .endAt(pastDate)
                .build()
        }).toThrow('must be in the future')
    })
    
    it('should validate minimum duration', () => {
        const start = new Date('2025-01-01')
        const end = new Date('2025-01-02') // Too short
        
        expect(() => {
            new PromoBuilder()
                .name('Dev 2025')
                .beginAt(start)
                .endAt(end)
                .build()
        }).toThrow('minimum duration')
    })
})

Immutability Tests
#

describe('PromoBuilder - Immutability', () => {
    
    it('should not affect previous builds', () => {
        const builder = new PromoBuilder()
        
        const promo1 = builder
            .name('Promo A')
            .beginAt(new Date('2025-01-01'))
            .endAt(new Date('2025-06-30'))
            .build()
        
        const promo2 = builder
            .name('Promo B')
            .beginAt(new Date('2025-07-01'))
            .endAt(new Date('2025-12-31'))
            .build()
        
        expect(promo1.getName()).toBe('Promo A')
        expect(promo2.getName()).toBe('Promo B')
        expect(promo1).not.toBe(promo2)
    })
})

Performance Tests
#

describe('PromoBuilder - Performance', () => {
    
    it('should build 1000 promos quickly', () => {
        const start = Date.now()
        
        for (let i = 0; i < 1000; i++) {
            new PromoBuilder()
                .name(`Promo ${i}`)
                .beginAt(new Date())
                .endAt(new Date())
                .build()
        }
        
        const duration = Date.now() - start
        expect(duration).toBeLessThan(1000) // < 1 second
    })
})

Conclusion
#

Summary
#

The Builder pattern is ideal for building complex objects with validation:

Advantages Disadvantages
Readable and expressive code More verbose code
Centralized validation Additional class
Objects always valid Attribute duplication
Complete flexibility Increased complexity
Possible immutability Memory overhead

When to Use It?
#

Use the Builder for:

  • Objects with many parameters (more than 4-5)
  • Complex validation before construction
  • Immutable objects after creation
  • Multi-step logical construction
  • Public APIs requiring good usability

Avoid the Builder for:

  • Simple objects with few parameters
  • Objects without validation
  • Performance-critical scenarios (builder overhead)
  • Internal objects not exposed
  • Simple value objects

Related

The Singleton Pattern - Guaranteeing a Single Instance
·11 mins· loading · loading
Design Back-End
Single Responsibility Principle (SRP) - Invoice Class Refactoring
·13 mins· loading · loading
Design Back-End
Interfaces in Object-Oriented Programming - SOLID Principle and Practical Example
·6 mins· loading · loading
Design Back-End
Database Modeling - The Langlois Case
·6 mins· loading · loading
Design Back-End
Documentation and Google Workspace - Practical Guide for Developers
·11 mins· loading · loading
Documentation
Algorithms - Complete Guide
··23 mins· loading · loading
Back-End Front-End