class Database {
static #instance = null;
constructor(url) {
if (Database.#instance) return Database.#instance;
this.url = url;
Database.#instance = this;
}
}
const db1 = new Database('localhost');
const db2 = new Database('remote');
console.log(db1 === db2); // true
console.log(db2.url); // "localhost" — first instance wins
// Simplest approach: ES module singleton (db.js)
// export const db = new Database('localhost');
// Every importer shares the same instance automatically
Why it matters: Singletons appear in nearly every production app for shared resources—misimplementing them leads to duplicate connections, inconsistent state, and race conditions that are hard to debug.
Real applications: Mongoose database connection pool (Node.js), Redux store, Angular root-level services (providedIn root), and browser localStorage are all Singleton instances shared across the entire app.
Common mistakes: Re-implementing class-based Singleton when importing an ES module already provides singleton behavior for free—every named module export is effectively a Singleton, making manual static instance tracking redundant boilerplate.
class EventEmitter {
#events = {};
on(event, fn) {
(this.#events[event] ??= []).push(fn);
return () => this.off(event, fn); // returns unsubscribe fn
}
off(event, fn) {
this.#events[event] = this.#events[event]?.filter(f => f !== fn);
}
emit(event, ...args) {
this.#events[event]?.forEach(fn => fn(...args));
}
}
const bus = new EventEmitter();
const unsub = bus.on('data', (msg) => console.log('Received:', msg));
bus.emit('data', 'Hello World'); // "Received: Hello World"
unsub(); // clean up listener
bus.emit('data', 'Again'); // silence — unsubscribed
Why it matters: Observer is fundamental to event-driven programming; interviewers test it to see if you understand decoupling and whether you handle listener cleanup to prevent memory leaks.
Real applications: Node.js EventEmitter, DOM addEventListener, RxJS Observables (used heavily in Angular), and Redux store subscription callbacks all implement the Observer pattern.
Common mistakes: Forgetting to call the returned unsubscribe function when a component unmounts, causing stale callbacks to fire long after the subscriber is destroyed and producing memory leaks.
function createUser(role) {
const base = { role, createdAt: Date.now() };
switch (role) {
case 'admin': return { ...base, canDelete: true, canBan: true };
case 'editor': return { ...base, canPublish: true, canDelete: false };
case 'viewer': return { ...base, readOnly: true };
default: throw new Error(`Unknown role: ${role}`);
}
}
const admin = createUser('admin');
const viewer = createUser('viewer');
console.log(admin.canDelete); // true
console.log(viewer.readOnly); // true
// Adding a new role only requires a new case —
// no changes needed in any consuming code
Why it matters: Interviewers use the Factory pattern to test knowledge of the Open/Closed Principle and whether you can decouple object creation from business logic to make systems extensible and testable.
Real applications: React.createElement(), Angular component factories, Express router factory, and Knex.js query builder all use the Factory pattern to abstract construction complexity behind a stable function interface.
Common mistakes: Letting callers use new directly on concrete classes instead of going through the factory, which creates tight coupling that makes swapping implementations difficult without touching every call site.
// IIFE Module — encapsulates private state
const Counter = (() => {
let count = 0; // private — not accessible outside
return {
increment() { return ++count; },
decrement() { return --count; },
reset() { count = 0; },
getCount() { return count; }
};
})();
Counter.increment(); // 1
Counter.increment(); // 2
Counter.getCount(); // 2
// Counter.count // undefined — private!
// ES Module equivalent (counter.js)
// let count = 0;
// export const increment = () => ++count;
// export const getCount = () => count;
Why it matters: Understanding the Module pattern demonstrates mastery of closures and scope—two of the most-tested JavaScript fundamentals—and shows how encapsulation is achieved without class syntax.
Real applications: jQuery's entire codebase was wrapped in an IIFE; Webpack and Rollup bundle ES modules using this concept; Node.js CommonJS wraps each file in a function providing closure-based private scope.
Common mistakes: Believing properties on the returned public object are private—only variables declared in the closure scope are truly private; properties of the returned object can be freely read and modified by outside code.
// Payment strategies as a lookup table
const paymentStrategies = {
creditCard: (amount) => `Charging $${amount} to credit card`,
paypal: (amount) => `Sending $${amount} via PayPal`,
crypto: (amount) => `Broadcasting $${amount} in BTC`
};
function checkout(amount, strategy) {
const fn = paymentStrategies[strategy];
if (!fn) throw new Error(`Unknown strategy: ${strategy}`);
return fn(amount);
}
checkout(100, 'paypal'); // "Sending $100 via PayPal"
checkout(100, 'creditCard'); // "Charging $100 to credit card"
// Adding a new method requires no changes elsewhere
paymentStrategies.applePay = (amount) => `Apple Pay: $${amount}`;
Why it matters: Strategy is a top interview pattern because it directly demonstrates the Open/Closed Principle—interviewers want to see you replace conditionals with a data-driven dispatch table that supports extension without modification.
Real applications: Passport.js (pluggable authentication strategies), Webpack loader selection, Lodash sort comparators, and form validation libraries all use this pattern to support pluggable, swappable behaviors.
Common mistakes: Using a switch statement instead of a lookup object—switch is harder to extend and cannot be modified at runtime, whereas a plain object allows dynamic strategy registration and deletion.
// Decorator adding retry logic to any async function
function withRetry(fn, retries = 3) {
return async function(...args) {
for (let attempt = 0; attempt < retries; attempt++) {
try {
return await fn.apply(this, args);
} catch (err) {
if (attempt === retries - 1) throw err;
console.warn(`Retry ${attempt + 1}/${retries}`);
}
}
};
}
// Decorator adding memoization
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn.apply(this, args));
return cache.get(key);
};
}
// Stack decorators — memoize then wrap with retries
const fetchUser = withRetry(memoize(getUserById), 3);
Why it matters: Decorators are a core interview topic because they demonstrate mastery of higher-order functions and cross-cutting concerns—logging, caching, authentication, and retry logic in production are almost always implemented as decorators.
Real applications: Angular uses class decorators extensively (@Component, @Injectable); NestJS mirrors this with @Controller; React Higher-Order Components (HOCs) are the Decorator pattern applied to UI components.
Common mistakes: Forgetting to use fn.apply(this, args) inside the wrapper, which breaks methods that rely on the correct this context and loses the original function's name from stack traces.
function createValidator(target, schema) {
return new Proxy(target, {
set(obj, prop, value) {
if (prop in schema && !schema[prop](value)) {
throw new TypeError(`Invalid value for "${prop}": ${value}`);
}
obj[prop] = value;
return true; // required: must return true on success
},
get(obj, prop) {
return Reflect.get(obj, prop); // delegate default behavior
}
});
}
const user = createValidator({}, {
age: (v) => Number.isInteger(v) && v >= 0,
name: (v) => typeof v === 'string' && v.length > 0
});
user.name = 'Alice'; // OK
user.age = 30; // OK
// user.age = -1; // TypeError: Invalid value for "age": -1
Why it matters: Proxy is a senior-level interview topic testing deep JavaScript knowledge—it shows understanding of meta-programming and how modern frameworks implement reactivity and validation under the hood.
Real applications: Vue 3's reactivity system is built entirely on Proxy; MobX 5+ uses Proxy for observable objects; Immer uses Proxy to track mutations and produce immutable state updates.
Common mistakes: Forgetting to return true from the set trap in strict mode, which silently causes a TypeError; and not using Reflect methods inside traps, which can break invariants for non-configurable or non-writable properties.
class Range {
constructor(start, end) { this.start = start; this.end = end; }
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true };
}
};
}
}
for (const n of new Range(1, 5)) console.log(n); // 1 2 3 4 5
console.log([...new Range(1, 3)]); // [1, 2, 3]
// Generator equivalent — much more concise
function* range(start, end) {
for (let i = start; i <= end; i++) yield i;
}
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]
Why it matters: The iterator protocol underpins generators, async iteration, and all for...of loops—understanding it is essential for working with custom data structures, lazy sequences, and streaming data pipelines.
Real applications: Node.js readable streams implement async iterators; MongoDB driver cursors use async iteration; D3.js data joins rely on iterability; Immer draft patches iterate over change sets.
Common mistakes: Implementing Symbol.iterator but returning the object itself as the iterator without a proper next() method, which breaks iteration when the same iterable is used in nested loops simultaneously.
class EventBus {
#handlers = new Map();
subscribe(event, handler) {
if (!this.#handlers.has(event)) this.#handlers.set(event, []);
this.#handlers.get(event).push(handler);
return () => this.unsubscribe(event, handler); // returns unsub fn
}
unsubscribe(event, handler) {
const list = this.#handlers.get(event) ?? [];
this.#handlers.set(event, list.filter(h => h !== handler));
}
publish(event, payload) {
this.#handlers.get(event)?.forEach(h => h(payload));
}
}
const bus = new EventBus();
// Components talk through the bus — never directly to each other
const unsubCart = bus.subscribe('item:added', ({ name }) => console.log(`Cart: +${name}`));
const unsubBadge = bus.subscribe('item:added', ({ count }) => console.log(`Badge: ${count}`));
bus.publish('item:added', { name: 'Laptop', count: 1 });
unsubCart(); // only badge handler receives events now
Why it matters: The Mediator is the architectural foundation of event-driven UIs—interviewers test it to see if you can design loosely coupled systems where adding a feature doesn't require modifying existing components.
Real applications: Redux (the store mediates all state updates), Angular NgRx action bus, DOM CustomEvent dispatching, and Socket.io rooms all act as mediators coordinating multiple components.
Common mistakes: Letting the mediator grow into a God Object that accumulates business logic—the mediator should only coordinate communication routing, not contain domain logic that belongs inside individual components.
class History {
#stack = [];
execute(command) {
command.execute();
this.#stack.push(command);
}
undo() { this.#stack.pop()?.undo(); }
}
class SetValueCommand {
constructor(obj, key, newValue) {
this.obj = obj;
this.key = key;
this.newValue = newValue;
this.oldValue = obj[key]; // snapshot old value for undo
}
execute() { this.obj[this.key] = this.newValue; }
undo() { this.obj[this.key] = this.oldValue; }
}
const state = { color: 'red' };
const history = new History();
history.execute(new SetValueCommand(state, 'color', 'blue'));
console.log(state.color); // "blue"
history.execute(new SetValueCommand(state, 'color', 'green'));
console.log(state.color); // "green"
history.undo();
console.log(state.color); // "blue"
Why it matters: Command is a crucial pattern for any app requiring undo/redo—interviewers use it to test if you can design composable, reversible operations rather than directly mutating state in place.
Real applications: VS Code's edit history, Figma's undo stack, Redux dispatch actions (each action is a Command object), and database transaction logs all implement the Command pattern for reversible operations.
Common mistakes: Not capturing the previous state as a snapshot at command construction time—if the old value is read lazily at undo time, concurrent mutations between execute and undo produce incorrect rollbacks.
class RequestBuilder {
#config = { method: 'GET', headers: {}, body: null, timeout: 5000 };
url(url) { this.#config.url = url; return this; }
method(m) { this.#config.method = m.toUpperCase(); return this; }
header(k, v) { this.#config.headers[k] = v; return this; }
body(data) { this.#config.body = JSON.stringify(data); return this; }
timeout(ms) { this.#config.timeout = ms; return this; }
async send() {
const { url, method, headers, body, timeout } = this.#config;
const ctrl = new AbortController();
const id = setTimeout(() => ctrl.abort(), timeout);
try {
return await fetch(url, { method, headers, body, signal: ctrl.signal });
} finally {
clearTimeout(id);
}
}
}
const res = await new RequestBuilder()
.url('https://api.example.com/users')
.method('POST')
.header('Authorization', 'Bearer token')
.body({ name: 'Alice' })
.timeout(3000)
.send();
Why it matters: Builder demonstrates fluent API design—a skill interviewers look for in senior developers who design SDKs, test fixtures, or configuration-heavy objects that resist argument-ordering bugs and are self-documenting at call sites.
Real applications: Knex.js SQL query builder, Jest assertion chaining, Mongoose query chains (.find().sort().limit()), and Axios request configuration all implement the Builder pattern's fluent interface.
Common mistakes: Mutating the same internal config object across multiple uses of the same builder instance—if the builder is reused without resetting, previous configuration options silently leak into subsequent builds.
// Third-party logger with a verbose API
class WinstonLogger {
log(level, message, meta) {
console.log(`[${level.toUpperCase()}] ${message}`, meta);
}
}
// App expects a simple { info, warn, error } interface
class LoggerAdapter {
#logger;
constructor(logger) { this.#logger = logger; }
info(msg, meta) { this.#logger.log('info', msg, meta ?? {}); }
warn(msg, meta) { this.#logger.log('warn', msg, meta ?? {}); }
error(msg, meta) { this.#logger.log('error', msg, meta ?? {}); }
}
const logger = new LoggerAdapter(new WinstonLogger());
logger.info('Server started', { port: 3000 });
// [INFO] Server started { port: 3000 }
// Swap to a different logger with no app code changes:
// const logger = new LoggerAdapter(new DatadogLogger());
Why it matters: Adapters are how production codebases safely integrate external libraries—interviewers use this pattern to assess whether you can isolate third-party dependencies so they can be swapped without causing ripple-effect changes.
Real applications: Axios browser/Node.js adapters, Passport.js authentication strategy adapters, TypeORM database driver adapters, and React's synthetic event system (adapts native browser events) all use this pattern.
Common mistakes: Embedding business logic inside the adapter—adapters should only translate interfaces, not add feature behavior; mixing concerns turns a simple adapter into a hidden source of bugs that violates the single-responsibility principle.
function memoize(fn, maxSize = Infinity) {
const cache = new Map();
return function memoized(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
// Evict oldest entry when cache exceeds size limit
if (cache.size >= maxSize) cache.delete(cache.keys().next().value);
cache.set(key, result);
return result;
};
}
// Recursive memoization — must reference the memoized outer var
const fib = memoize(function fibonacci(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2); // hits cache on recursive calls
});
console.time('first'); fib(40); console.timeEnd('first'); // ~2ms
console.time('second'); fib(40); console.timeEnd('second'); // ~0ms
Why it matters: Memoization is a core performance optimization that interviewers expect you to implement from scratch—it appears in algorithm problems (top-down DP), React's useMemo/useCallback, and API response caching.
Real applications: React useMemo and React.memo, Reselect (Redux selector memoization), Lodash _.memoize, and Vue 3 computed properties all use memoization to skip redundant computation when inputs haven't changed.
Common mistakes: Applying memoization to impure functions—functions with side effects or non-deterministic output like Date.now() or Math.random()—which causes the cache to return stale or incorrect results on subsequent calls.
// Complex subsystem
class HttpClient { request(cfg) { return fetch(cfg.url, cfg); } }
class TokenStore { getToken() { return localStorage.getItem('token'); } }
class RateLimiter { check(url) { /* enforce limits */ return true; } }
class Logger { log(msg) { console.log('[API]', msg); } }
// Facade — single clean entry point over all subsystems
class ApiClient {
#http = new HttpClient();
#tokens = new TokenStore();
#limiter = new RateLimiter();
#logger = new Logger();
get(url) { return this.#request('GET', url); }
post(url, body) { return this.#request('POST', url, body); }
#request(method, url, body) {
if (!this.#limiter.check(url)) throw new Error('Rate limited');
this.#logger.log(`${method} ${url}`);
return this.#http.request({
url, method,
headers: { Authorization: `Bearer ${this.#tokens.getToken()}` },
body: body ? JSON.stringify(body) : undefined
});
}
}
const api = new ApiClient();
api.get('/users'); // handles auth, logging, rate-limiting
api.post('/users', data); // same simplicity
Why it matters: Facade is one of the most practically useful patterns—interviewers look for it when discussing API client design, SDK abstraction, or hiding third-party complexity behind a clean internal interface that's easy to swap.
Real applications: jQuery (facade over the DOM API), Axios (facade over XMLHttpRequest/Fetch), Mongoose (facade over the MongoDB driver), and AWS SDK clients are all facades that hide low-level protocol complexity.
Common mistakes: Over-relying on the facade until it stops covering an edge case, then reaching through it to call the subsystem directly—this defeats the abstraction and creates hidden coupling that makes the facade misleading.
class Pipeline {
#middlewares = [];
use(fn) { this.#middlewares.push(fn); return this; }
async run(ctx) {
let index = 0;
const next = async () => {
if (index < this.#middlewares.length) {
await this.#middlewares[index++](ctx, next);
}
};
await next();
return ctx;
}
}
const pipeline = new Pipeline()
.use(async (ctx, next) => {
ctx.startTime = Date.now();
await next();
ctx.duration = Date.now() - ctx.startTime; // post-handler timing
})
.use(async (ctx, next) => {
if (!ctx.token) { ctx.error = '401 Unauthorized'; return; } // stop chain
await next();
})
.use(async (ctx, next) => {
ctx.result = `Processed: ${ctx.input}`;
});
const ctx = await pipeline.run({ token: 'abc', input: 'hello' });
console.log(ctx.result); // "Processed: hello"
console.log(ctx.duration); // ~0ms
Why it matters: Chain of Responsibility is the architectural pattern behind middleware stacks that every Node.js developer uses daily—interviewers test it to see if you understand layered request processing and where each concern belongs.
Real applications: Express.js and Koa middleware stacks, Angular HttpClient interceptors, Axios request/response interceptors, and browser service workers (fetch event chain) all implement this pattern.
Common mistakes: Forgetting to call next() in middleware that should not short-circuit the chain—silently dropping a request without calling next or sending a response is the most common cause of hanging API calls in Express apps.