Episode 9 — System Design / 9.3 — Creational Design Patterns
9.3 Quick Revision -- Creational Design Patterns
Pattern Comparison Table
| Pattern | Intent | Creates | Complexity | JS Idiom |
|---|---|---|---|---|
| Singleton | One instance, global access | 1 instance | Low | Module-level export |
| Factory Method | Delegate "which class?" to a factory | 1 product at a time | Low-Med | Factory function + map |
| Abstract Factory | Create families of related products | Family of products | Med-High | Factory class per family |
| Builder | Step-by-step complex construction | 1 complex object | Medium | Fluent interface + build() |
| Prototype | Clone existing objects | Copies of a template | Low | structuredClone() |
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
| Pattern | One 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
| Method | Type | Date | Map/Set | Functions | Circular Refs |
|---|---|---|---|---|---|
{ ...obj } | Shallow | Ref | Ref | Ref | N/A |
Object.assign() | Shallow | Ref | Ref | Ref | N/A |
JSON parse/stringify | Deep | Becomes string | Lost | Lost | Throws |
structuredClone() | Deep | Preserved | Preserved | Throws | Handled |
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
| Mistake | Fix |
|---|---|
| Singleton everywhere (global state abuse) | Use DI; reserve Singleton for truly unique resources |
| Factory for a single class | Just use new directly |
| Abstract Factory when you only need one product type | Use Factory Method |
| Builder for 2-3 required params | Use a constructor |
| Shallow copy when deep copy is needed | Use structuredClone() |
| Not resetting Singleton in tests | Add _resetInstance() or use module mocking |
Forgetting build() validation | Always validate in build() |
SOLID Alignment
| Pattern | SOLID Principle | How |
|---|---|---|
| Factory Method | Open/Closed | Add new products without modifying existing code |
| Abstract Factory | Dependency Inversion | Client depends on abstract factory, not concrete |
| Builder | Single Responsibility | Separates 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!