Lesson 6 / 25

Builder

Assemble complex objects step by step.

Readable construction

Intent: separate the construction of a complex object from its representation, so it can be built step by step and validated before use. Builders shine when an object has many optional parts, ordering rules or validation: HTTP requests, SQL queries, test data, email messages. A fluent builder returns this from each step so calls chain. Use it when a constructor would need many optional parameters (the "telescoping constructor" smell) or when construction needs validation. Avoid it when an options object is enough: in TypeScript and Python, named/optional parameters or an object literal with defaults often replace a builder entirely. Builders remain idiomatic in Java, where there are no named arguments.

An email builder

Validates before producing an immutable message.

interface Email {
  readonly to: string[];
  readonly subject: string;
  readonly html: string;
  readonly cc: string[];
  readonly attachments: { name: string; data: Uint8Array }[];
}

class EmailBuilder {
  private to: string[] = [];
  private cc: string[] = [];
  private subject = '';
  private html = '';
  private attachments: Email['attachments'] = [];

  addTo(address: string) { this.to.push(address); return this; }
  addCc(address: string) { this.cc.push(address); return this; }
  withSubject(s: string) { this.subject = s; return this; }
  withHtml(h: string) { this.html = h; return this; }
  attach(name: string, data: Uint8Array) { this.attachments.push({ name, data }); return this; }

  build(): Email {
    if (this.to.length === 0) throw new Error('At least one recipient is required');
    if (!this.subject) throw new Error('Subject is required');
    return Object.freeze({ to: [...this.to], cc: [...this.cc], subject: this.subject,
                           html: this.html, attachments: [...this.attachments] });
  }
}

const email = new EmailBuilder()
  .addTo('asha@example.com')
  .withSubject('Your invoice')
  .withHtml('<p>Thanks for your order.</p>')
  .attach('invoice.pdf', pdfBytes)
  .build();

Builders are great for test data

A test-data builder with sensible defaults lets each test override only the fields it cares about, for example aUser().withRole('admin').build(), which keeps tests short and intention-revealing.

Quick check: In TypeScript, what often replaces a simple builder?

  • A Visitor
  • A Singleton
  • An Observer
  • An options object with defaults
Answer

An options object with defaults — Object literals give named, optional parameters.