Design Patterns & Architecture
Design Patterns & Architecture
Section titled “Design Patterns & Architecture”Modern JavaScript applications require solid architectural foundations. This guide covers essential design patterns and architectural approaches that will help you build maintainable, scalable applications.
Creational Patterns
Section titled “Creational Patterns”Factory Pattern
Section titled “Factory Pattern”Create objects without specifying their exact classes.
// Factory for creating different types of usersclass UserFactory { static createUser(type, data) { switch (type) { case 'admin': return new AdminUser(data); case 'customer': return new CustomerUser(data); case 'guest': return new GuestUser(data); default: throw new Error(`Unknown user type: ${type}`); } }}
class AdminUser { constructor({ name, email, permissions }) { this.name = name; this.email = email; this.permissions = permissions || ['read', 'write', 'delete']; this.role = 'admin'; }
canAccess(resource) { return true; // Admin can access everything }}
class CustomerUser { constructor({ name, email, subscription }) { this.name = name; this.email = email; this.subscription = subscription; this.role = 'customer'; }
canAccess(resource) { return this.subscription.includes(resource); }}
// Usageconst admin = UserFactory.createUser('admin', { name: 'Alice', email: 'alice@admin.com'});
const customer = UserFactory.createUser('customer', { name: 'Bob', email: 'bob@customer.com', subscription: ['basic-features']});
Builder Pattern
Section titled “Builder Pattern”Construct complex objects step by step.
class QueryBuilder { constructor() { this.query = { select: [], from: '', where: [], orderBy: [], limit: null }; }
select(fields) { this.query.select = Array.isArray(fields) ? fields : [fields]; return this; }
from(table) { this.query.from = table; return this; }
where(condition) { this.query.where.push(condition); return this; }
orderBy(field, direction = 'ASC') { this.query.orderBy.push(`${field} ${direction}`); return this; }
limit(count) { this.query.limit = count; return this; }
build() { let sql = `SELECT ${this.query.select.join(', ')}`; sql += ` FROM ${this.query.from}`;
if (this.query.where.length > 0) { sql += ` WHERE ${this.query.where.join(' AND ')}`; }
if (this.query.orderBy.length > 0) { sql += ` ORDER BY ${this.query.orderBy.join(', ')}`; }
if (this.query.limit) { sql += ` LIMIT ${this.query.limit}`; }
return sql; }}
// Usageconst query = new QueryBuilder() .select(['name', 'email', 'created_at']) .from('users') .where('active = 1') .where('verified = 1') .orderBy('created_at', 'DESC') .limit(10) .build();
console.log(query);// SELECT name, email, created_at FROM users WHERE active = 1 AND verified = 1 ORDER BY created_at DESC LIMIT 10
Singleton Pattern
Section titled “Singleton Pattern”Ensure a class has only one instance.
class Logger { constructor() { if (Logger.instance) { return Logger.instance; }
this.logs = []; this.level = 'info'; Logger.instance = this; }
log(level, message) { const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, message }; this.logs.push(logEntry);
if (this.shouldLog(level)) { console.log(`[${timestamp}] ${level.toUpperCase()}: ${message}`); } }
shouldLog(level) { const levels = ['debug', 'info', 'warn', 'error']; return levels.indexOf(level) >= levels.indexOf(this.level); }
setLevel(level) { this.level = level; }
getLogs() { return [...this.logs]; }}
// Usageconst logger1 = new Logger();const logger2 = new Logger();
console.log(logger1 === logger2); // true - same instance
logger1.log('info', 'Application started');logger2.log('error', 'Something went wrong');
console.log(logger1.getLogs().length); // 2 - shared state
Structural Patterns
Section titled “Structural Patterns”Adapter Pattern
Section titled “Adapter Pattern”Allow incompatible interfaces to work together.
// Legacy payment processorclass LegacyPaymentProcessor { makePayment(amount) { return { success: true, transactionId: Math.random().toString(36).substr(2, 9), amount: amount, status: 'completed' }; }}
// Modern payment interfaceclass ModernPaymentProcessor { processPayment({ amount, currency, metadata }) { return { success: true, id: Math.random().toString(36).substr(2, 9), amount, currency, status: 'success', metadata }; }}
// Adapter to make legacy processor work with modern interfaceclass PaymentAdapter { constructor(legacyProcessor) { this.legacyProcessor = legacyProcessor; }
processPayment({ amount, currency = 'USD', metadata = {} }) { const result = this.legacyProcessor.makePayment(amount);
// Adapt the response to modern format return { success: result.success, id: result.transactionId, amount: result.amount, currency, status: result.status === 'completed' ? 'success' : 'failed', metadata }; }}
// Usageconst legacyProcessor = new LegacyPaymentProcessor();const modernProcessor = new ModernPaymentProcessor();const adaptedProcessor = new PaymentAdapter(legacyProcessor);
// All processors now have the same interfaceconst processors = [modernProcessor, adaptedProcessor];
processors.forEach(processor => { const result = processor.processPayment({ amount: 100, currency: 'USD', metadata: { orderId: '12345' } }); console.log(result);});
Decorator Pattern
Section titled “Decorator Pattern”Add new functionality to objects dynamically.
// Base coffee classclass Coffee { constructor() { this.description = 'Simple coffee'; this.cost = 2.00; }
getDescription() { return this.description; }
getCost() { return this.cost; }}
// Decorator base classclass CoffeeDecorator extends Coffee { constructor(coffee) { super(); this.coffee = coffee; }
getDescription() { return this.coffee.getDescription(); }
getCost() { return this.coffee.getCost(); }}
// Concrete decoratorsclass MilkDecorator extends CoffeeDecorator { constructor(coffee) { super(coffee); }
getDescription() { return this.coffee.getDescription() + ', milk'; }
getCost() { return this.coffee.getCost() + 0.50; }}
class SugarDecorator extends CoffeeDecorator { constructor(coffee) { super(coffee); }
getDescription() { return this.coffee.getDescription() + ', sugar'; }
getCost() { return this.coffee.getCost() + 0.25; }}
class WhipDecorator extends CoffeeDecorator { constructor(coffee) { super(coffee); }
getDescription() { return this.coffee.getDescription() + ', whip'; }
getCost() { return this.coffee.getCost() + 0.75; }}
// Usagelet myCoffee = new Coffee();console.log(`${myCoffee.getDescription()}: $${myCoffee.getCost()}`);
myCoffee = new MilkDecorator(myCoffee);console.log(`${myCoffee.getDescription()}: $${myCoffee.getCost()}`);
myCoffee = new SugarDecorator(myCoffee);console.log(`${myCoffee.getDescription()}: $${myCoffee.getCost()}`);
myCoffee = new WhipDecorator(myCoffee);console.log(`${myCoffee.getDescription()}: $${myCoffee.getCost()}`);// Simple coffee, milk, sugar, whip: $3.50
Behavioral Patterns
Section titled “Behavioral Patterns”Observer Pattern
Section titled “Observer Pattern”Define a one-to-many dependency between objects.
class EventEmitter { constructor() { this.events = {}; }
on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); }
off(event, callback) { if (!this.events[event]) return;
this.events[event] = this.events[event].filter(cb => cb !== callback); }
emit(event, data) { if (!this.events[event]) return;
this.events[event].forEach(callback => { try { callback(data); } catch (error) { console.error(`Error in event handler for ${event}:`, error); } }); }
once(event, callback) { const onceWrapper = (data) => { callback(data); this.off(event, onceWrapper); }; this.on(event, onceWrapper); }}
// Usage example: Shopping cartclass ShoppingCart extends EventEmitter { constructor() { super(); this.items = []; this.total = 0; }
addItem(item) { this.items.push(item); this.total += item.price; this.emit('itemAdded', { item, total: this.total }); }
removeItem(itemId) { const itemIndex = this.items.findIndex(item => item.id === itemId); if (itemIndex !== -1) { const item = this.items.splice(itemIndex, 1)[0]; this.total -= item.price; this.emit('itemRemoved', { item, total: this.total }); } }
checkout() { const order = { items: [...this.items], total: this.total, timestamp: new Date() };
this.emit('checkoutStarted', order);
// Simulate async checkout setTimeout(() => { this.items = []; this.total = 0; this.emit('checkoutCompleted', order); }, 1000); }}
// Observersconst cart = new ShoppingCart();
cart.on('itemAdded', ({ item, total }) => { console.log(`Added ${item.name} - Total: $${total}`);});
cart.on('itemRemoved', ({ item, total }) => { console.log(`Removed ${item.name} - Total: $${total}`);});
cart.on('checkoutCompleted', (order) => { console.log('Order completed:', order);});
// Usagecart.addItem({ id: 1, name: 'Laptop', price: 999 });cart.addItem({ id: 2, name: 'Mouse', price: 25 });cart.checkout();
Strategy Pattern
Section titled “Strategy Pattern”Define a family of algorithms and make them interchangeable.
// Payment strategiesclass PaymentStrategy { pay(amount) { throw new Error('Payment method must be implemented'); }}
class CreditCardPayment extends PaymentStrategy { constructor(cardNumber, expiryDate, cvv) { super(); this.cardNumber = cardNumber; this.expiryDate = expiryDate; this.cvv = cvv; }
pay(amount) { console.log(`Paid $${amount} using Credit Card ending in ${this.cardNumber.slice(-4)}`); return { success: true, method: 'credit_card', amount, transactionId: this.generateTransactionId() }; }
generateTransactionId() { return 'cc_' + Math.random().toString(36).substr(2, 9); }}
class PayPalPayment extends PaymentStrategy { constructor(email) { super(); this.email = email; }
pay(amount) { console.log(`Paid $${amount} using PayPal account ${this.email}`); return { success: true, method: 'paypal', amount, transactionId: this.generateTransactionId() }; }
generateTransactionId() { return 'pp_' + Math.random().toString(36).substr(2, 9); }}
class BitcoinPayment extends PaymentStrategy { constructor(walletAddress) { super(); this.walletAddress = walletAddress; }
pay(amount) { console.log(`Paid $${amount} using Bitcoin wallet ${this.walletAddress.slice(0, 8)}...`); return { success: true, method: 'bitcoin', amount, transactionId: this.generateTransactionId() }; }
generateTransactionId() { return 'btc_' + Math.random().toString(36).substr(2, 9); }}
// Context classclass PaymentProcessor { constructor() { this.strategy = null; }
setPaymentStrategy(strategy) { this.strategy = strategy; }
processPayment(amount) { if (!this.strategy) { throw new Error('Payment strategy not set'); }
return this.strategy.pay(amount); }}
// Usageconst processor = new PaymentProcessor();
// Pay with credit cardprocessor.setPaymentStrategy(new CreditCardPayment('1234567890123456', '12/25', '123'));processor.processPayment(100);
// Pay with PayPalprocessor.setPaymentStrategy(new PayPalPayment('user@example.com'));processor.processPayment(50);
// Pay with Bitcoinprocessor.setPaymentStrategy(new BitcoinPayment('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'));processor.processPayment(75);
Command Pattern
Section titled “Command Pattern”Encapsulate requests as objects.
// Command interfaceclass Command { execute() { throw new Error('Execute method must be implemented'); }
undo() { throw new Error('Undo method must be implemented'); }}
// Receiverclass TextEditor { constructor() { this.content = ''; }
insertText(text, position = this.content.length) { this.content = this.content.slice(0, position) + text + this.content.slice(position); }
deleteText(position, length) { const deleted = this.content.slice(position, position + length); this.content = this.content.slice(0, position) + this.content.slice(position + length); return deleted; }
getContent() { return this.content; }}
// Concrete commandsclass InsertCommand extends Command { constructor(editor, text, position) { super(); this.editor = editor; this.text = text; this.position = position; }
execute() { this.editor.insertText(this.text, this.position); }
undo() { this.editor.deleteText(this.position, this.text.length); }}
class DeleteCommand extends Command { constructor(editor, position, length) { super(); this.editor = editor; this.position = position; this.length = length; this.deletedText = ''; }
execute() { this.deletedText = this.editor.deleteText(this.position, this.length); }
undo() { this.editor.insertText(this.deletedText, this.position); }}
// Invokerclass EditorInvoker { constructor() { this.history = []; this.currentPosition = -1; }
execute(command) { // Remove any commands after current position (for redo functionality) this.history = this.history.slice(0, this.currentPosition + 1);
command.execute(); this.history.push(command); this.currentPosition++; }
undo() { if (this.currentPosition >= 0) { const command = this.history[this.currentPosition]; command.undo(); this.currentPosition--; } }
redo() { if (this.currentPosition < this.history.length - 1) { this.currentPosition++; const command = this.history[this.currentPosition]; command.execute(); } }}
// Usageconst editor = new TextEditor();const invoker = new EditorInvoker();
// Execute commandsinvoker.execute(new InsertCommand(editor, 'Hello ', 0));console.log(editor.getContent()); // "Hello "
invoker.execute(new InsertCommand(editor, 'World!', 6));console.log(editor.getContent()); // "Hello World!"
invoker.execute(new DeleteCommand(editor, 5, 6));console.log(editor.getContent()); // "Hello"
// Undo operationsinvoker.undo();console.log(editor.getContent()); // "Hello World!"
invoker.undo();console.log(editor.getContent()); // "Hello "
// Redo operationsinvoker.redo();console.log(editor.getContent()); // "Hello World!"
Architectural Patterns
Section titled “Architectural Patterns”Model-View-Controller (MVC)
Section titled “Model-View-Controller (MVC)”Separate application logic into three interconnected components.
// Modelclass UserModel { constructor() { this.users = []; this.observers = []; }
addObserver(observer) { this.observers.push(observer); }
notifyObservers() { this.observers.forEach(observer => observer.update()); }
addUser(user) { const newUser = { id: Date.now(), ...user, createdAt: new Date() }; this.users.push(newUser); this.notifyObservers(); return newUser; }
removeUser(id) { this.users = this.users.filter(user => user.id !== id); this.notifyObservers(); }
getUsers() { return [...this.users]; }
getUserById(id) { return this.users.find(user => user.id === id); }}
// Viewclass UserView { constructor() { this.container = document.createElement('div'); this.container.className = 'user-list'; }
render(users) { this.container.innerHTML = '';
users.forEach(user => { const userElement = document.createElement('div'); userElement.className = 'user-item'; userElement.innerHTML = ` <h3>${user.name}</h3> <p>Email: ${user.email}</p> <p>Created: ${user.createdAt.toLocaleDateString()}</p> <button onclick="userController.removeUser(${user.id})">Remove</button> `; this.container.appendChild(userElement); }); }
getContainer() { return this.container; }
update() { // This method is called by the controller when model changes }}
// Controllerclass UserController { constructor(model, view) { this.model = model; this.view = view;
// Set up the observer relationship this.model.addObserver(this); }
addUser(userData) { if (!userData.name || !userData.email) { throw new Error('Name and email are required'); }
return this.model.addUser(userData); }
removeUser(id) { this.model.removeUser(id); }
getAllUsers() { return this.model.getUsers(); }
// Observer method - called when model changes update() { this.view.render(this.model.getUsers()); }}
// Usageconst userModel = new UserModel();const userView = new UserView();const userController = new UserController(userModel, userView);
// Make controller globally available for button clickswindow.userController = userController;
// Add some usersuserController.addUser({ name: 'Alice', email: 'alice@example.com' });userController.addUser({ name: 'Bob', email: 'bob@example.com' });
// Append view to documentdocument.body.appendChild(userView.getContainer());
Module Pattern
Section titled “Module Pattern”Organize code into reusable, encapsulated modules.
// Revealing Module Patternconst Calculator = (function() { // Private variables let history = []; let precision = 2;
// Private methods function round(value) { return Math.round(value * Math.pow(10, precision)) / Math.pow(10, precision); }
function addToHistory(operation, operands, result) { history.push({ operation, operands, result, timestamp: new Date() }); }
// Public interface return { add(a, b) { const result = round(a + b); addToHistory('add', [a, b], result); return result; },
subtract(a, b) { const result = round(a - b); addToHistory('subtract', [a, b], result); return result; },
multiply(a, b) { const result = round(a * b); addToHistory('multiply', [a, b], result); return result; },
divide(a, b) { if (b === 0) { throw new Error('Division by zero'); } const result = round(a / b); addToHistory('divide', [a, b], result); return result; },
setPrecision(newPrecision) { precision = Math.max(0, Math.min(10, newPrecision)); },
getHistory() { return [...history]; },
clearHistory() { history = []; } };})();
// Usageconsole.log(Calculator.add(5, 3)); // 8console.log(Calculator.multiply(2.1, 3.7)); // 7.77console.log(Calculator.getHistory());
// Modern ES6 Module Patternclass AdvancedCalculator { #history = []; #precision = 2;
#round(value) { return Math.round(value * Math.pow(10, this.#precision)) / Math.pow(10, this.#precision); }
#addToHistory(operation, operands, result) { this.#history.push({ operation, operands, result, timestamp: new Date() }); }
add(a, b) { const result = this.#round(a + b); this.#addToHistory('add', [a, b], result); return result; }
power(base, exponent) { const result = this.#round(Math.pow(base, exponent)); this.#addToHistory('power', [base, exponent], result); return result; }
sqrt(value) { if (value < 0) { throw new Error('Cannot calculate square root of negative number'); } const result = this.#round(Math.sqrt(value)); this.#addToHistory('sqrt', [value], result); return result; }
getStatistics() { const operations = this.#history.map(entry => entry.operation); const counts = operations.reduce((acc, op) => { acc[op] = (acc[op] || 0) + 1; return acc; }, {});
return { totalOperations: this.#history.length, operationCounts: counts, lastOperation: this.#history[this.#history.length - 1] }; }}
export { Calculator, AdvancedCalculator };
This comprehensive guide covers the most important design patterns and architectural approaches in JavaScript. These patterns help create maintainable, scalable, and well-structured applications.
Practice implementing these patterns in the playground to solidify your understanding of software architecture.