Design Patterns
Common design patterns in frontend development
Design patterns are reusable solutions to common software design problems. In frontend interviews, you are expected to recognise patterns in existing code and explain how to apply them. More importantly, you should know why a pattern solves a specific problem β not just what it is called.
Creational Patterns
Singleton
Ensures a class has only one instance and provides global access. In JavaScript, module singletons are idiomatic.
// Module-level singleton β the module system caches the export
let instance;
class Store {
constructor() { if (instance) return instance; instance = this; this.state = {}; }
}
export const store = new Store(); // same object every import
Factory
Creates objects without specifying the exact class. Useful when the type of object depends on input.
function createNotification(type, message) {
const base = { message, timestamp: Date.now() };
const types = {
success: () => ({ ...base, icon: 'β', color: 'green' }),
error: () => ({ ...base, icon: 'β', color: 'red' }),
info: () => ({ ...base, icon: 'βΉ', color: 'blue' }),
};
return (types[type] || types.info)();
}
Structural Patterns
Observer / Pub-Sub
One-to-many dependency: when a subject changes state, all its observers are notified. This is the pattern behind DOM events, React's useState, and most state management libraries.
class EventEmitter {
#listeners = new Map();
on(event, cb) {
if (!this.#listeners.has(event)) this.#listeners.set(event, []);
this.#listeners.get(event).push(cb);
return () => this.off(event, cb); // return unsubscribe function
}
emit(event, data) { (this.#listeners.get(event) || []).forEach(cb => cb(data)); }
off(event, cb) { this.#listeners.set(event, (this.#listeners.get(event) || []).filter(l => l !== cb)); }
}
Decorator
Wraps an object to add behaviour without modifying the original. Higher-order components and higher-order functions are decorator implementations.
// Higher-order function decorator β adds logging
function withLogging(fn) {
return function(...args) {
console.log('Calling', fn.name, 'with', args);
const result = fn.apply(this, args);
console.log('Result:', result);
return result;
};
}
const loggedFetch = withLogging(fetch);
Behavioural Patterns
Strategy
Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Eliminates large if/else chains.
const sortStrategies = {
byName: (a, b) => a.name.localeCompare(b.name),
byDate: (a, b) => new Date(b.date) - new Date(a.date),
byPrice: (a, b) => a.price - b.price,
};
function sortItems(items, strategy = 'byName') {
return [...items].sort(sortStrategies[strategy]);
}
Command
Encapsulates a request as an object, enabling undo/redo and queuing.
class TextEditor {
#history = [];
#text = '';
execute(command) { this.#history.push(command); this.#text = command.execute(this.#text); }
undo() { const cmd = this.#history.pop(); if (cmd) this.#text = cmd.undo(this.#text); }
}
const insertCommand = (text) => ({
execute: (current) => current + text,
undo: (current) => current.slice(0, -text.length),
});
React-Specific Patterns
- Compound Components β related components share implicit state via Context (e.g.
<Select><Option>) - Render Props β pass a function as a prop to share stateful logic (largely superseded by hooks)
- Custom Hooks β extract and reuse stateful logic across components
- Provider Pattern β inject dependencies via Context rather than prop-drilling