Skip to content

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.

Create objects without specifying their exact classes.

// Factory for creating different types of users
class 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);
}
}
// Usage
const admin = UserFactory.createUser('admin', {
name: 'Alice',
email: 'alice@admin.com'
});
const customer = UserFactory.createUser('customer', {
name: 'Bob',
email: 'bob@customer.com',
subscription: ['basic-features']
});

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;
}
}
// Usage
const 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

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];
}
}
// Usage
const 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

Allow incompatible interfaces to work together.

// Legacy payment processor
class LegacyPaymentProcessor {
makePayment(amount) {
return {
success: true,
transactionId: Math.random().toString(36).substr(2, 9),
amount: amount,
status: 'completed'
};
}
}
// Modern payment interface
class 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 interface
class 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
};
}
}
// Usage
const legacyProcessor = new LegacyPaymentProcessor();
const modernProcessor = new ModernPaymentProcessor();
const adaptedProcessor = new PaymentAdapter(legacyProcessor);
// All processors now have the same interface
const processors = [modernProcessor, adaptedProcessor];
processors.forEach(processor => {
const result = processor.processPayment({
amount: 100,
currency: 'USD',
metadata: { orderId: '12345' }
});
console.log(result);
});

Add new functionality to objects dynamically.

// Base coffee class
class Coffee {
constructor() {
this.description = 'Simple coffee';
this.cost = 2.00;
}
getDescription() {
return this.description;
}
getCost() {
return this.cost;
}
}
// Decorator base class
class CoffeeDecorator extends Coffee {
constructor(coffee) {
super();
this.coffee = coffee;
}
getDescription() {
return this.coffee.getDescription();
}
getCost() {
return this.coffee.getCost();
}
}
// Concrete decorators
class 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;
}
}
// Usage
let 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

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 cart
class 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);
}
}
// Observers
const 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);
});
// Usage
cart.addItem({ id: 1, name: 'Laptop', price: 999 });
cart.addItem({ id: 2, name: 'Mouse', price: 25 });
cart.checkout();

Define a family of algorithms and make them interchangeable.

// Payment strategies
class 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 class
class 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);
}
}
// Usage
const processor = new PaymentProcessor();
// Pay with credit card
processor.setPaymentStrategy(new CreditCardPayment('1234567890123456', '12/25', '123'));
processor.processPayment(100);
// Pay with PayPal
processor.setPaymentStrategy(new PayPalPayment('user@example.com'));
processor.processPayment(50);
// Pay with Bitcoin
processor.setPaymentStrategy(new BitcoinPayment('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'));
processor.processPayment(75);

Encapsulate requests as objects.

// Command interface
class Command {
execute() {
throw new Error('Execute method must be implemented');
}
undo() {
throw new Error('Undo method must be implemented');
}
}
// Receiver
class 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 commands
class 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);
}
}
// Invoker
class 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();
}
}
}
// Usage
const editor = new TextEditor();
const invoker = new EditorInvoker();
// Execute commands
invoker.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 operations
invoker.undo();
console.log(editor.getContent()); // "Hello World!"
invoker.undo();
console.log(editor.getContent()); // "Hello "
// Redo operations
invoker.redo();
console.log(editor.getContent()); // "Hello World!"

Separate application logic into three interconnected components.

// Model
class 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);
}
}
// View
class 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
}
}
// Controller
class 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());
}
}
// Usage
const userModel = new UserModel();
const userView = new UserView();
const userController = new UserController(userModel, userView);
// Make controller globally available for button clicks
window.userController = userController;
// Add some users
userController.addUser({ name: 'Alice', email: 'alice@example.com' });
userController.addUser({ name: 'Bob', email: 'bob@example.com' });
// Append view to document
document.body.appendChild(userView.getContainer());

Organize code into reusable, encapsulated modules.

// Revealing Module Pattern
const 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 = [];
}
};
})();
// Usage
console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.multiply(2.1, 3.7)); // 7.77
console.log(Calculator.getHistory());
// Modern ES6 Module Pattern
class 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.