Design Patterns
Reusable solutions to common problems — the essential JavaScript patterns: module, singleton, observer, factory, strategy, and more.
A class-based Singleton only works if everyone calls getInstance(). Nothing stops new AppConfig(): it quietly builds a second, empty instance, and the settings everyone else saved seem to have disappeared. No error is thrown. It happens all the time on teams, because new is what people type out of habit.
class AppConfig {
static #instance = null;
#settings = {};
static getInstance() {
if (!AppConfig.#instance) {
AppConfig.#instance = new AppConfig();
}
return AppConfig.#instance;
}
set(key, value) { this.#settings[key] = value; }
get(key) { return this.#settings[key]; }
}
AppConfig.getInstance().set("theme", "dark");
// elsewhere, someone writes `new` out of habit:
const config = new AppConfig();
console.log(config.get("theme"));
// undefinedNamed solutions to problems you'll encounter repeatedly
Every experienced developer has solved the same problems dozens of times. Design patterns are the named, agreed-upon solutions to the most common ones. Knowing their names lets you say "use the Observer pattern here" instead of describing the whole thing from scratch.
Module pattern — private state with closures
// The module pattern creates private state — accessible only through
// the returned public interface:
function createCounter(initial = 0) {
let count = initial; // private — not accessible from outside
return {
increment() { count++; },
decrement() { count--; },
reset() { count = initial; },
value() { return count; },
};
}
const counter = createCounter(10);
counter.increment();
counter.increment();
console.log(counter.value()); // 12
// counter.count → undefined (private!)Singleton — one instance only
// Useful for: config, loggers, database connections
class AppConfig {
static #instance = null;
#settings = {};
static getInstance() {
if (!AppConfig.#instance) {
AppConfig.#instance = new AppConfig();
}
return AppConfig.#instance;
}
set(key, value) { this.#settings[key] = value; }
get(key) { return this.#settings[key]; }
}
const config1 = AppConfig.getInstance();
const config2 = AppConfig.getInstance();
config1.set("theme", "dark");
console.log(config2.get("theme")); // "dark" — same instance!
console.log(config1 === config2); // trueMake the rule enforce itself: let the constructor run only when getInstance() calls it, and throw a clear error otherwise. (In ES modules there's an even simpler singleton: export one instance, export const config = new Config();. A module is evaluated only once, so every file that imports it gets the same object.)
class AppConfig {
static #instance = null;
static #creating = false;
#settings = {};
constructor() {
if (!AppConfig.#creating) throw new Error("Use AppConfig.getInstance(), not new");
}
static getInstance() {
if (!AppConfig.#instance) {
AppConfig.#creating = true;
AppConfig.#instance = new AppConfig();
AppConfig.#creating = false;
}
return AppConfig.#instance;
}
set(key, value) { this.#settings[key] = value; }
get(key) { return this.#settings[key]; }
}
AppConfig.getInstance().set("theme", "dark");
try {
new AppConfig();
} catch (e) {
console.log(e.message);
}
console.log(AppConfig.getInstance().get("theme"));
// Use AppConfig.getInstance(), not new
// darkObserver — subscribe to events
// The pattern behind: DOM events, Node's EventEmitter, Redux, Vue reactivity
class Store {
#state;
#listeners = [];
constructor(initialState) { this.#state = initialState; }
getState() { return { ...this.#state }; } // return copy, not reference
setState(updates) {
this.#state = { ...this.#state, ...updates };
this.#listeners.forEach(fn => fn(this.#state)); // notify all observers
}
subscribe(listener) {
this.#listeners.push(listener);
return () => { // return unsubscribe function
this.#listeners = this.#listeners.filter(l => l !== listener);
};
}
}
const store = new Store({ user: null, theme: "light" });
const unsub = store.subscribe(state => console.log("State changed:", state.theme));
store.setState({ theme: "dark" }); // logs: State changed: dark
store.setState({ user: "Alice" }); // logs: State changed: dark (user added)
unsub(); // stop listening
store.setState({ theme: "light" }); // no log — unsubscribedFactory — create objects without specifying their class
// Instead of: new Dog(), new Cat() — use a factory:
function createAnimal(type, name) {
const base = {
name,
toString() { return `${this.type} named ${this.name}`; },
};
const animals = {
dog: () => ({ ...base, type: "Dog", speak: () => "Woof!" }),
cat: () => ({ ...base, type: "Cat", speak: () => "Meow!" }),
bird: () => ({ ...base, type: "Bird", speak: () => "Tweet!" }),
};
const factory = animals[type.toLowerCase()];
if (!factory) throw new Error(`Unknown animal: ${type}`);
return factory();
}
const dog = createAnimal("dog", "Rex");
const cat = createAnimal("cat", "Whiskers");
console.log(dog.speak()); // "Woof!"
console.log(cat.toString()); // "Cat named Whiskers"Official Sources
- MDN Web Docs — JavaScript Guide
- Addy Osmani — Learning JavaScript Design Patterns
- patterns.dev — Modern JavaScript Patterns
- Refactoring Guru — Design Patterns in JavaScript
The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.