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