Foundation 5 of 6 · 25 min

Factory, Builder, Singleton and Command

Four patterns that show up constantly, and the honest answer to when each one is overkill.

✓ Choose between a constructor, a factory and a builder✓ Explain why a Singleton is usually a smell, and what to do instead✓ Model undo and redo with commands
Say it in 30 seconds
  • 1 placeWhere the choice of class to create lives
  • 2 stacksHistory keeps done and undone, for undo and redo
  • 10 millionCharacters a snapshot undo stores for 1,000 edits
Show what to say

“I decide which class to create in one place, a factory or registry, so a new kind is one register call. I skip the Builder when an options object will do, and I replace a Singleton with one instance created at startup and passed in. For undo and redo I model each action as a command with do and undo, on two stacks.”

Trade-offs

  • Registry vs one switch: a registry adds kinds without editing old code; for few fixed kinds, one switch in one place is enough.
  • Singleton vs injected instance: both give one in production; injection makes the dependency visible and testable.
  • Snapshot undo vs commands: snapshots are simplest but cost document size times edits; commands store less but each needs a correct undo.

Bring it up before they ask

Clear the redo stack when a new edit follows an undo, and make each command remember exactly what it changed, so undo restores exactly that.

0 of 0 sections ticked
30-second answer

Decide which class to create in one place. Skip the Builder when an options object will do, and pass one instance in instead of using a Singleton. Model undo as commands with do and undo.

Create it by name

Think of a vending machine: press B4 and the snack drops. That is a factory.

Read more · 93 words

A string like "sms" arrives from config. How many places know how to turn it into an object?

Add push and you must find every switch on the channel; miss one and push users fail in production. Put the choice in one place.

Callers say channels.create(kind). A new kind is one register call and no edit to old code.

ToolUse it whenExample
ConstructorYou know the typenew Sms()
Static factoryCreation needs a name or checkDuration.ofMinutes(5)
RegistryNew kinds arrive as datachannels.create(kind)
A factory method is a factory with a conveyor belt: you order a product and it decides which shape comes out.
// The same switch, pasted wherever the channel matters.
export function send(channel, to, text) {
  switch (channel) {
    case 'email': return `email to ${to}: ${text}`;
    case 'sms': return `sms to ${to}: ${text.slice(0, 160)}`;
    default: throw new Error(`unknown channel: ${channel}`);
  }
}

export function costInCents(channel) {
  switch (channel) {
    case 'email': return 0;
    case 'sms': return 4;
    default: throw new Error(`unknown channel: ${channel}`);
  }
}
// Adding 'push' means finding every switch like these, in every file, and editing each one.
class Email {
  send(to, text) { return `email to ${to}: ${text}`; }
  get costInCents() { return 0; }
}

class Sms {
  send(to, text) { return `sms to ${to}: ${text.slice(0, 160)}`; }
  get costInCents() { return 4; }
}

// The one place that knows which kinds exist. A new kind is one register() call.
export class ChannelRegistry {
  #makers = new Map();

  register(kind, make) {
    this.#makers.set(kind, make);
    return this;
  }

  create(kind) {
    const make = this.#makers.get(kind);
    if (!make) throw new Error(`unknown channel: ${kind}`);
    return make();
  }
}

export const defaultChannels = () =>
  new ChannelRegistry().register('email', () => new Email()).register('sms', () => new Sms());

Builder, and when it is overkill

A constructor with seven parameters, four optional, gives new Email(a, b, null, null, true). Nobody can read that.

Read more · 93 words

A Builder collects the parts one at a time and creates the object at the end, checking the whole combination once.

Reach for it when:

  • Many parts are optional.
  • A rule spans several fields and must hold before any object exists.
  • The result is immutable, but the parts arrive over time.

In JavaScript and Python you need one less often. An options object does the job in one line:

Add a validity check to the constructor and you have most of a builder. Do not write one for a class with three fields.

export class EmailBuilder {
  #to = [];
  #cc = [];
  #subject = '';
  #body = '';

  to(address) { this.#to.push(address); return this; }
  cc(address) { this.#cc.push(address); return this; }
  subject(text) { this.#subject = text; return this; }
  body(text) { this.#body = text; return this; }

  // Every rule about a valid combination lives here. The result is frozen, and
  // it holds copies, so changing the builder later cannot change an email already built.
  build() {
    if (this.#to.length === 0) throw new Error('an email needs a recipient');
    if (this.#subject === '') throw new Error('an email needs a subject');
    return Object.freeze({
      to: Object.freeze([...this.#to]),
      cc: Object.freeze([...this.#cc]),
      subject: this.#subject,
      body: this.#body,
    });
  }
}
new Email({ to: ['ann@x.com'], subject: 'Lunch', cc: ['bob@x.com'] })
Email(to=("ann@x.com",), subject="Lunch", cc=("bob@x.com",))

Singleton: one instance, no global

One town clock everyone checks sounds neat until a test needs its own.

Read more · 60 words

That is a Singleton: TicketNumbers.get().next() is a global with a nicer name.

  • Hidden dependency. issueTicket(vehicle) does not say it uses shared state.
  • Tests leak. A test passes alone and fails in a suite.
  • You cannot have two.

Instead, create it once near main and hand it in (dependency injection). Production has one; a test builds its own.

A singleton is one throne: everybody who asks for it is pointed at the same single chair.
// One shared instance, reachable from anywhere.
export class TicketNumbers {
  static #instance;
  #last = 0;

  static get() {
    return (TicketNumbers.#instance ??= new TicketNumbers());
  }

  next() { return ++this.#last; }
}

// A hidden dependency: nothing in the signature says this function touches shared state.
export const issueTicket = (vehicle) => ({ vehicle, number: TicketNumbers.get().next() });

Command: an action as an object

Saving a full copy before every edit is simple, but 1,000 edits of a 10,000-character text store 10 million characters.

Read more · 58 words

The Command pattern stores the action instead, and each command can do and undo itself.

  • Erase remembers what it removed, so undo restores exactly that.
  • History has two stacks. undo moves a command from done to undone; redo moves it back.
  • A new edit clears the redo stack.

A new Paste is a new class; History is untouched.

export class Doc {
  text = '';
}

// A command knows how to do and undo itself. It keeps what it needs to reverse the change.
export class Type {
  constructor(doc, text) { this.doc = doc; this.text = text; }
  do() { this.doc.text += this.text; }
  undo() { this.doc.text = this.doc.text.slice(0, this.doc.text.length - this.text.length); }
}

export class Erase {
  constructor(doc, count) { this.doc = doc; this.count = count; }
  do() {
    const keep = Math.max(0, this.doc.text.length - this.count);
    this.removed = this.doc.text.slice(keep); // may be fewer than count characters
    this.doc.text = this.doc.text.slice(0, keep);
  }
  undo() { this.doc.text += this.removed; }
}

// Two stacks. History never looks inside a command, so a new kind of edit needs no change here.
export class History {
  #done = [];
  #undone = [];

  run(cmd) {
    cmd.do();
    this.#done.push(cmd);
    this.#undone.length = 0; // a new edit ends the old future
  }

  undo() {
    const cmd = this.#done.pop();
    if (!cmd) return false;
    cmd.undo();
    this.#undone.push(cmd);
    return true;
  }

  redo() {
    const cmd = this.#undone.pop();
    if (!cmd) return false;
    cmd.do();
    this.#done.push(cmd);
    return true;
  }
}

Watch the design grow

Step through: each stage adds classes without changing the old ones.

Stage 1 of 4

The naive editor

The editor changes the Doc directly. Undo has nowhere to live, so the usual patch is a full copy of the text before every edit: easy to write, heavy on memory, and redo needs a second pile of copies.

Undo and redo with commandsAn Editor owns a Doc and a History. Type and Erase are commands that implement Command, so History can run, undo and redo any of them. Macro is also a Command and wraps many, so a group of edits undoes in one step.Editordoc: Doctype(text)erase(count)undo()redo()Doctext: string

An Editor owns a Doc and a History. Type and Erase are commands that implement Command, so History can run, undo and redo any of them. Macro is also a Command and wraps many, so a group of edits undoes in one step.

  • ◆ composition (owns)
  • ◇ aggregation
  • △ inheritance / implements (dashed)
  • → association · ⇢ dependency

One undo step for many edits

"Replace all" changes forty places, but one Ctrl+Z should undo it. A group of commands is itself a command.

Read more · 26 words

Macro undoes its steps in reverse, and History cannot tell it from a single edit. Add a third command and watch one undo remove all three.

// Several commands that undo as one. It is a command too, so History needs no change.
export class Macro {
  constructor(commands) { this.commands = commands; }

  do() {
    for (const cmd of this.commands) cmd.do();
  }

  // Reverse order matters: the last change sits on top of the earlier ones.
  undo() {
    for (const cmd of [...this.commands].reverse()) cmd.undo();
  }
}
try-it.js
// Change the edits, or add a second Macro, and watch what one undo does.
const doc = new Doc();
const history = new History();

history.run(new Type(doc, 'hello world'));
history.run(new Macro([new Erase(doc, 5), new Type(doc, 'there')]));
console.log(doc.text);               // hello there

history.undo();
console.log(doc.text);               // hello world  (the whole macro, in one step)

history.undo();
console.log(JSON.stringify(doc.text)); // ""

history.redo();
console.log(doc.text);               // hello world

history.run(new Type(doc, '!'));
console.log(history.redo());         // false: a new edit clears the redo stack

Which one, when

Each pattern answers one question. Not asking it? Skip the pattern.

Read more · 76 words
PatternReach for it whenSkip it when
Factory or registryThe class depends on dataThe type is known at the call
BuilderMany optional partsAn options object will do
SingletonNever as a globalAlways, for state
CommandUndo, queues, logsNothing reverses or records it

The over-engineering trap

Read more · 37 words

Practise with the text editor exercise. Last lesson: the LLD playbook.

Follow-up questions

Is a Singleton ever the right answer?

One instance is often right. A global way to reach it rarely is. Create it at startup and pass it in.

What about an action that cannot be undone, such as sending an email?

Keep it off the undo stack; for a reversal, add a compensating action, like a correction email.

Check yourself

Question 1 of 3

Score 2 out of 3 or better to mark this page complete.

1. A config file says channel: 'sms'. Today there are two channels, next quarter twelve, and plugins will add more. Where should the 'which class?' decision live?

Code it yourself

Write the class in the browser editor, in JavaScript or Python. Each exercise runs against hidden tests, and the step-by-step solution unlocks after a wrong submission.

Next: You have the pieces. Last: how to run a 45-minute LLD interview with them.