Episode 9 — System Design / 9.3 — Creational Design Patterns

9.3 Quick Revision -- Creational Design Patterns


Pattern Comparison Table

PatternIntentCreatesComplexityJS Idiom
SingletonOne instance, global access1 instanceLowModule-level export
Factory MethodDelegate "which class?" to a factory1 product at a timeLow-MedFactory function + map
Abstract FactoryCreate families of related productsFamily of productsMed-HighFactory class per family
BuilderStep-by-step complex construction1 complex objectMediumFluent interface + build()
PrototypeClone existing objectsCopies of a templateLowstructuredClone()

When-To-Use Decision Guide

START
  |
  +-- Need exactly ONE instance? --> SINGLETON
  |
  +-- Need to create objects without knowing the exact class?
  |     |
  |     +-- ONE product type?  --> FACTORY METHOD
  |     +-- FAMILY of related products? --> ABSTRACT FACTORY
  |
  +-- Object has many optional params / complex construction? --> BUILDER
  |
  +-- Need many similar objects / creation is expensive? --> PROTOTYPE
  |
  +-- Simple object, few params, one class? --> Just use `new`

One-Line Summaries

PatternOne Sentence
Singleton"There can be only one -- and everyone gets the same one."
Factory Method"Tell me WHAT you need, I'll decide HOW to make it."
Abstract Factory"Pick a family, and I'll give you matching parts."
Builder"Let me build it piece by piece so you don't drown in parameters."
Prototype"Don't build from scratch -- clone the template and tweak."

Code Snippets

Singleton (Module-Based)

// logger.js
class Logger {
  constructor() { this.logs = []; }
  log(msg) { this.logs.push({ time: Date.now(), msg }); console.log(msg); }
}
module.exports = new Logger(); // Same instance everywhere

Singleton (Class-Based with Async)

class DB {
  static _instance = null;
  static _promise = null;
  static async getInstance() {
    if (DB._instance) return DB._instance;
    if (!DB._promise) DB._promise = DB._init();
    return DB._promise;
  }
  static async _init() {
    const conn = await connect();
    DB._instance = new DB(conn);
    DB._promise = null;
    return DB._instance;
  }
}

Factory Method (Function)

function createNotification(type) {
  const map = { email: EmailNotif, sms: SMSNotif, push: PushNotif };
  const Cls = map[type];
  if (!Cls) throw new Error(`Unknown: ${type}`);
  return new Cls();
}

Factory Method (Registry Class)

class Factory {
  static reg = new Map();
  static register(type, cls) { Factory.reg.set(type, cls); }
  static create(type, opts) {
    const Cls = Factory.reg.get(type);
    if (!Cls) throw new Error(`Unknown: ${type}`);
    return new Cls(opts);
  }
}
Factory.register('email', EmailNotif);

Abstract Factory

class MaterialFactory {
  createButton() { return new MaterialButton(); }
  createInput()  { return new MaterialInput(); }
  createModal()  { return new MaterialModal(); }
}

class BootstrapFactory {
  createButton() { return new BootstrapButton(); }
  createInput()  { return new BootstrapInput(); }
  createModal()  { return new BootstrapModal(); }
}

// Client never knows concrete classes
function render(factory) {
  const btn = factory.createButton();
  const inp = factory.createInput();
  btn.render(); inp.render();
}

Builder (Fluent)

class RequestBuilder {
  constructor() { this._method = 'GET'; this._url = ''; this._headers = {}; this._body = null; }
  get(url)  { this._method = 'GET';  this._url = url; return this; }
  post(url) { this._method = 'POST'; this._url = url; return this; }
  json(data) { this._body = data; this._headers['Content-Type'] = 'application/json'; return this; }
  header(k, v) { this._headers[k] = v; return this; }
  build() {
    if (!this._url) throw new Error('URL required');
    return Object.freeze({ method: this._method, url: this._url, headers: this._headers, body: this._body });
  }
}

Prototype (Clone)

class Config {
  constructor(data) { Object.assign(this, data); }
  clone() { return new Config(structuredClone(this)); }
}

const base = new Config({ host: 'localhost', port: 3000, db: { name: 'app' } });
const staging = base.clone();
staging.db.name = 'app_staging'; // base.db.name still 'app'

Shallow vs Deep Copy Cheat Sheet

MethodTypeDateMap/SetFunctionsCircular Refs
{ ...obj }ShallowRefRefRefN/A
Object.assign()ShallowRefRefRefN/A
JSON parse/stringifyDeepBecomes stringLostLostThrows
structuredClone()DeepPreservedPreservedThrowsHandled

Pattern Relationships

Singleton ---- controls HOW MANY (exactly one)
     |
     +-- Often HOLDS a Factory (e.g., singleton PaymentGateway uses a factory internally)

Factory Method ---- controls WHAT gets created (one product)
     |
     +-- Abstract Factory uses Factory Methods for each product in the family

Abstract Factory ---- controls WHAT FAMILY gets created
     |
     +-- Products within a family are guaranteed compatible

Builder ---- controls HOW something is constructed (step by step)
     |
     +-- Can use Factory internally for individual parts
     +-- Resulting object can be a Prototype for cloning

Prototype ---- controls HOW many copies exist (via cloning)
     |
     +-- Original can be built by a Builder
     +-- Registry can act as a Factory of clones

Common Mistakes

MistakeFix
Singleton everywhere (global state abuse)Use DI; reserve Singleton for truly unique resources
Factory for a single classJust use new directly
Abstract Factory when you only need one product typeUse Factory Method
Builder for 2-3 required paramsUse a constructor
Shallow copy when deep copy is neededUse structuredClone()
Not resetting Singleton in testsAdd _resetInstance() or use module mocking
Forgetting build() validationAlways validate in build()

SOLID Alignment

PatternSOLID PrincipleHow
Factory MethodOpen/ClosedAdd new products without modifying existing code
Abstract FactoryDependency InversionClient depends on abstract factory, not concrete
BuilderSingle ResponsibilitySeparates construction from representation
Singleton--Doesn't directly map; can violate DI if overused
Prototype--Operational pattern; orthogonal to SOLID

Interview Quick Answers

"What's the difference between Factory Method and Abstract Factory?" Factory Method creates one product; Abstract Factory creates a family of compatible products.

"When would you NOT use a Singleton?" When you might need multiple instances later, when it hides dependencies making testing hard, or when different modules need different configurations.

"Builder vs config object?" Builder when: validation needed, many optional params, immutability desired, complex construction order. Config object when: simple, few options, no validation needed.

"Why clone instead of construct?" When construction is expensive (DB loads, API calls, heavy computation) and many similar objects are needed.

"How do you test a Singleton?" Reset instance between tests with a _resetInstance() method, or better, use dependency injection so you can pass mocks directly.


Five-Pattern Checklist

Before choosing a pattern, ask:

  • Do I need exactly ONE instance? --> Singleton
  • Do I need to choose between product classes at runtime? --> Factory Method
  • Do I need families of related, compatible products? --> Abstract Factory
  • Does my object have many optional fields or complex construction? --> Builder
  • Is object creation expensive and I need many copies? --> Prototype
  • Is it simple with few params and one class? --> Just use new!