Design patterns are proven solutions to common problems. Here's how to implement classic patterns using modern JavaScript features.
Creational Patterns#
Factory Pattern#
1// Create objects without specifying exact class
2class UserFactory {
3 static create(type, data) {
4 switch (type) {
5 case 'admin':
6 return new AdminUser(data);
7 case 'member':
8 return new MemberUser(data);
9 case 'guest':
10 return new GuestUser(data);
11 default:
12 throw new Error(`Unknown user type: ${type}`);
13 }
14 }
15}
16
17class AdminUser {
18 constructor({ name, email }) {
19 this.name = name;
20 this.email = email;
21 this.permissions = ['read', 'write', 'delete', 'admin'];
22 }
23}
24
25class MemberUser {
26 constructor({ name, email }) {
27 this.name = name;
28 this.email = email;
29 this.permissions = ['read', 'write'];
30 }
31}
32
33// Usage
34const admin = UserFactory.create('admin', { name: 'John', email: 'john@example.com' });
35const member = UserFactory.create('member', { name: 'Jane', email: 'jane@example.com' });Builder Pattern#
1// Construct complex objects step by step
2class QueryBuilder {
3 constructor() {
4 this.query = {
5 select: [],
6 from: null,
7 where: [],
8 orderBy: [],
9 limit: null,
10 };
11 }
12
13 select(...fields) {
14 this.query.select.push(...fields);
15 return this;
16 }
17
18 from(table) {
19 this.query.from = table;
20 return this;
21 }
22
23 where(condition) {
24 this.query.where.push(condition);
25 return this;
26 }
27
28 orderBy(field, direction = 'ASC') {
29 this.query.orderBy.push({ field, direction });
30 return this;
31 }
32
33 limit(count) {
34 this.query.limit = count;
35 return this;
36 }
37
38 build() {
39 let sql = `SELECT ${this.query.select.join(', ')} FROM ${this.query.from}`;
40
41 if (this.query.where.length) {
42 sql += ` WHERE ${this.query.where.join(' AND ')}`;
43 }
44
45 if (this.query.orderBy.length) {
46 const orders = this.query.orderBy.map(o => `${o.field} ${o.direction}`);
47 sql += ` ORDER BY ${orders.join(', ')}`;
48 }
49
50 if (this.query.limit) {
51 sql += ` LIMIT ${this.query.limit}`;
52 }
53
54 return sql;
55 }
56}
57
58// Usage
59const query = new QueryBuilder()
60 .select('id', 'name', 'email')
61 .from('users')
62 .where('status = "active"')
63 .where('age > 18')
64 .orderBy('created_at', 'DESC')
65 .limit(10)
66 .build();Singleton Pattern#
1// Ensure only one instance exists
2class Database {
3 static #instance = null;
4
5 constructor() {
6 if (Database.#instance) {
7 return Database.#instance;
8 }
9
10 this.connection = this.connect();
11 Database.#instance = this;
12 }
13
14 static getInstance() {
15 if (!Database.#instance) {
16 Database.#instance = new Database();
17 }
18 return Database.#instance;
19 }
20
21 connect() {
22 console.log('Connecting to database...');
23 return { connected: true };
24 }
25
26 query(sql) {
27 return `Executing: ${sql}`;
28 }
29}
30
31// Or using module pattern (simpler)
32// db.js
33let connection = null;
34
35export function getConnection() {
36 if (!connection) {
37 connection = createConnection();
38 }
39 return connection;
40}Structural Patterns#
Adapter Pattern#
1// Make incompatible interfaces work together
2class OldPaymentSystem {
3 constructor() {
4 this.name = 'LegacyPay';
5 }
6
7 makePayment(amount, currency, cardNumber) {
8 return {
9 status: 'success',
10 transactionId: `LP-${Date.now()}`,
11 amount: amount,
12 };
13 }
14}
15
16class NewPaymentSystem {
17 constructor() {
18 this.name = 'ModernPay';
19 }
20
21 processPayment({ amount, currency, paymentMethod }) {
22 return {
23 success: true,
24 id: `MP-${Date.now()}`,
25 chargedAmount: amount,
26 };
27 }
28}
29
30// Adapter to make old system work with new interface
31class PaymentAdapter {
32 constructor(legacySystem) {
33 this.legacySystem = legacySystem;
34 }
35
36 processPayment({ amount, currency, paymentMethod }) {
37 const result = this.legacySystem.makePayment(
38 amount,
39 currency,
40 paymentMethod.cardNumber
41 );
42
43 return {
44 success: result.status === 'success',
45 id: result.transactionId,
46 chargedAmount: result.amount,
47 };
48 }
49}
50
51// Usage
52const paymentProcessor = new PaymentAdapter(new OldPaymentSystem());
53const result = paymentProcessor.processPayment({
54 amount: 100,
55 currency: 'USD',
56 paymentMethod: { cardNumber: '4111111111111111' },
57});Decorator Pattern#
1// Add behavior to objects dynamically
2class Coffee {
3 cost() {
4 return 5;
5 }
6
7 description() {
8 return 'Coffee';
9 }
10}
11
12// Decorators
13class MilkDecorator {
14 constructor(coffee) {
15 this.coffee = coffee;
16 }
17
18 cost() {
19 return this.coffee.cost() + 1;
20 }
21
22 description() {
23 return `${this.coffee.description()} + Milk`;
24 }
25}
26
27class SugarDecorator {
28 constructor(coffee) {
29 this.coffee = coffee;
30 }
31
32 cost() {
33 return this.coffee.cost() + 0.5;
34 }
35
36 description() {
37 return `${this.coffee.description()} + Sugar`;
38 }
39}
40
41// Usage
42let coffee = new Coffee();
43coffee = new MilkDecorator(coffee);
44coffee = new SugarDecorator(coffee);
45
46console.log(coffee.description()); // Coffee + Milk + Sugar
47console.log(coffee.cost()); // 6.5
48
49// Modern alternative: Higher-order functions
50const withLogging = (fn) => (...args) => {
51 console.log(`Calling ${fn.name} with`, args);
52 const result = fn(...args);
53 console.log(`Result:`, result);
54 return result;
55};
56
57const withTiming = (fn) => (...args) => {
58 const start = performance.now();
59 const result = fn(...args);
60 console.log(`${fn.name} took ${performance.now() - start}ms`);
61 return result;
62};
63
64const processData = withLogging(withTiming((data) => {
65 return data.map(x => x * 2);
66}));Proxy Pattern#
1// Control access to an object
2const userService = {
3 getUser(id) {
4 console.log(`Fetching user ${id}`);
5 return { id, name: 'John' };
6 },
7 deleteUser(id) {
8 console.log(`Deleting user ${id}`);
9 },
10};
11
12const userServiceProxy = new Proxy(userService, {
13 get(target, prop) {
14 const value = target[prop];
15
16 if (typeof value === 'function') {
17 return function (...args) {
18 console.log(`[AUDIT] Calling ${prop} with args:`, args);
19
20 // Access control
21 if (prop === 'deleteUser' && !currentUser.isAdmin) {
22 throw new Error('Unauthorized');
23 }
24
25 return value.apply(target, args);
26 };
27 }
28
29 return value;
30 },
31});
32
33// Caching proxy
34function createCachingProxy(target) {
35 const cache = new Map();
36
37 return new Proxy(target, {
38 apply(target, thisArg, args) {
39 const key = JSON.stringify(args);
40
41 if (cache.has(key)) {
42 console.log('Cache hit');
43 return cache.get(key);
44 }
45
46 console.log('Cache miss');
47 const result = target.apply(thisArg, args);
48 cache.set(key, result);
49 return result;
50 },
51 });
52}
53
54const cachedFetch = createCachingProxy(fetchData);Behavioral Patterns#
Observer Pattern#
1// Define a subscription mechanism
2class EventEmitter {
3 constructor() {
4 this.events = new Map();
5 }
6
7 on(event, listener) {
8 if (!this.events.has(event)) {
9 this.events.set(event, []);
10 }
11 this.events.get(event).push(listener);
12 return () => this.off(event, listener);
13 }
14
15 off(event, listener) {
16 const listeners = this.events.get(event);
17 if (listeners) {
18 const index = listeners.indexOf(listener);
19 if (index > -1) {
20 listeners.splice(index, 1);
21 }
22 }
23 }
24
25 emit(event, data) {
26 const listeners = this.events.get(event);
27 if (listeners) {
28 listeners.forEach(listener => listener(data));
29 }
30 }
31}
32
33// Usage
34const store = new EventEmitter();
35
36const unsubscribe = store.on('userLoggedIn', (user) => {
37 console.log(`Welcome, ${user.name}!`);
38});
39
40store.emit('userLoggedIn', { name: 'John' });
41
42unsubscribe(); // Clean upStrategy Pattern#
1// Define a family of algorithms
2const pricingStrategies = {
3 regular: (price) => price,
4 member: (price) => price * 0.9,
5 premium: (price) => price * 0.8,
6 sale: (price) => price * 0.5,
7};
8
9class ShoppingCart {
10 constructor() {
11 this.items = [];
12 this.pricingStrategy = pricingStrategies.regular;
13 }
14
15 setPricingStrategy(strategy) {
16 this.pricingStrategy = pricingStrategies[strategy] || pricingStrategies.regular;
17 }
18
19 addItem(item) {
20 this.items.push(item);
21 }
22
23 calculateTotal() {
24 const subtotal = this.items.reduce((sum, item) => sum + item.price, 0);
25 return this.pricingStrategy(subtotal);
26 }
27}
28
29// Usage
30const cart = new ShoppingCart();
31cart.addItem({ name: 'Laptop', price: 1000 });
32cart.addItem({ name: 'Mouse', price: 50 });
33
34console.log(cart.calculateTotal()); // 1050
35
36cart.setPricingStrategy('premium');
37console.log(cart.calculateTotal()); // 840Command Pattern#
1// Encapsulate requests as objects
2class Command {
3 execute() {
4 throw new Error('Must implement execute');
5 }
6
7 undo() {
8 throw new Error('Must implement undo');
9 }
10}
11
12class AddTextCommand extends Command {
13 constructor(document, text, position) {
14 super();
15 this.document = document;
16 this.text = text;
17 this.position = position;
18 }
19
20 execute() {
21 this.document.insert(this.text, this.position);
22 }
23
24 undo() {
25 this.document.delete(this.position, this.text.length);
26 }
27}
28
29class CommandHistory {
30 constructor() {
31 this.history = [];
32 this.position = -1;
33 }
34
35 execute(command) {
36 command.execute();
37 this.history = this.history.slice(0, this.position + 1);
38 this.history.push(command);
39 this.position++;
40 }
41
42 undo() {
43 if (this.position >= 0) {
44 this.history[this.position].undo();
45 this.position--;
46 }
47 }
48
49 redo() {
50 if (this.position < this.history.length - 1) {
51 this.position++;
52 this.history[this.position].execute();
53 }
54 }
55}
56
57// Usage
58const history = new CommandHistory();
59history.execute(new AddTextCommand(doc, 'Hello', 0));
60history.execute(new AddTextCommand(doc, ' World', 5));
61history.undo(); // Removes ' World'
62history.redo(); // Adds ' World' backConclusion#
Design patterns provide vocabulary and proven solutions for common problems. Modern JavaScript features (classes, modules, Proxy, higher-order functions) make many patterns cleaner to implement.
Don't force patterns where they're not needed. Use them when they solve real problems in your codebase.