Click ★ if you like the project. Your contributions are heartily ♡ welcome.
- Singleton Pattern
- Factory Pattern
- Observer Pattern
- Strategy Pattern
- Middleware Pattern
- Decorator Pattern
- Proxy Pattern
- Command Pattern
- Module Pattern
- Dependency Injection Pattern
- Repository Pattern
- Delegate Pattern
- Mediator Pattern
Common Use Cases:
- Shared database connection pool (Mongoose,
pg.Pool) reused across all modules - Application-wide configuration loaded once from environment variables
- Centralized logger instance (Winston, Pino) shared throughout the app
- In-memory cache or rate-limiter state shared across requests
- Feature flag manager initialized at startup
The Singleton pattern ensures only one instance of a class exists throughout the application lifecycle. In Node.js, the require() module cache makes every exported module a singleton by default.
// logger.js class Logger { constructor() { if (Logger.instance) { return Logger.instance; } this.logs = []; Logger.instance = this; } log(message) { this.logs.push({ message, timestamp: new Date() }); console.log(`[LOG]: ${message}`); } getLogs() { return this.logs; } } module.exports = new Logger(); // cached by require()
// app.js const logger = require('./logger'); const logger2 = require('./logger'); logger.log('Server started'); console.log(logger === logger2); // true — same instance
Node.js is single-threaded, so module-level singletons are inherently safe. Using Object.freeze() prevents external mutation:
// config.js class Config { constructor() { this.dbHost = process.env.DB_HOST || 'localhost'; this.dbPort = process.env.DB_PORT || 5432; this.appPort = process.env.PORT || 3000; } } const instance = Object.freeze(new Config()); module.exports = instance;
// server.js const config = require('./config'); console.log(config.dbHost); // 'localhost' config.dbHost = '127.0.0.1'; // silently fails due to freeze console.log(config.dbHost); // still 'localhost'
Common Use Cases:
- Creating different notification senders (Email, SMS, Push) based on user preference
- Instantiating database adapters (MySQL, PostgreSQL, MongoDB) based on environment config
- Generating different HTTP response formatters (JSON, XML, CSV) from a single endpoint
- Building different payment gateway clients (Stripe, PayPal, Razorpay) at runtime
- Creating transport strategies for logging (file, console, remote)
The Factory pattern provides an interface for creating objects without specifying the exact class. It centralizes object creation logic.
// notificationFactory.js class EmailNotification { send(message) { console.log(`Email sent: ${message}`); } } class SMSNotification { send(message) { console.log(`SMS sent: ${message}`); } } class PushNotification { send(message) { console.log(`Push notification sent: ${message}`); } } class NotificationFactory { static create(type) { switch (type) { case 'email': return new EmailNotification(); case 'sms': return new SMSNotification(); case 'push': return new PushNotification(); default: throw new Error(`Unknown notification type: ${type}`); } } } module.exports = NotificationFactory;
// app.js const NotificationFactory = require('./notificationFactory'); const email = NotificationFactory.create('email'); email.send('Welcome!'); // Email sent: Welcome! const sms = NotificationFactory.create('sms'); sms.send('Your OTP is 1234'); // SMS sent: Your OTP is 1234
- Factory: Creates one product type via a single factory method.
- Abstract Factory: Creates families of related products through multiple factory methods.
// abstractFactory.js class MySQLConnection { query(sql) { console.log(`MySQL query: ${sql}`); } } class MySQLTransaction { commit() { console.log('MySQL transaction committed'); } } class PostgreSQLConnection { query(sql) { console.log(`PostgreSQL query: ${sql}`); } } class PostgreSQLTransaction { commit() { console.log('PostgreSQL transaction committed'); } } class MySQLFactory { createConnection() { return new MySQLConnection(); } createTransaction() { return new MySQLTransaction(); } } class PostgreSQLFactory { createConnection() { return new PostgreSQLConnection(); } createTransaction() { return new PostgreSQLTransaction(); } } function getDatabaseFactory(dbType) { if (dbType === 'mysql') return new MySQLFactory(); if (dbType === 'postgresql') return new PostgreSQLFactory(); throw new Error(`Unsupported DB: ${dbType}`); } const factory = getDatabaseFactory('mysql'); const conn = factory.createConnection(); conn.query('SELECT * FROM users'); // MySQL query: SELECT * FROM users
Common Use Cases:
- Node.js core:
http.Server,fs.ReadStream,net.Socketall extendEventEmitter - Real-time order processing (order placed → email, inventory update, analytics)
- WebSocket event broadcasting (user joins, messages, disconnects)
- Internal pub/sub event bus decoupling microservice layers
- File watchers (
chokidar) triggering downstream rebuild or upload tasks - Audit logging by listening to domain events without modifying business logic
The Observer pattern defines a one-to-many dependency so that when one object changes state, all dependents are notified automatically. Node.js EventEmitter is a built-in implementation.
const EventEmitter = require('events'); class OrderService extends EventEmitter { placeOrder(order) { console.log(`Order placed: ${order.id}`); this.emit('orderPlaced', order); } } const orderService = new OrderService(); // Observers orderService.on('orderPlaced', (order) => { console.log(`[EmailService] Sending confirmation for order ${order.id}`); }); orderService.on('orderPlaced', (order) => { console.log(`[InventoryService] Reducing stock for ${order.item}`); }); orderService.on('orderPlaced', (order) => { console.log(`[AnalyticsService] Recording order event`); }); orderService.placeOrder({ id: 101, item: 'Laptop' }); // Order placed: 101 // [EmailService] Sending confirmation for order 101 // [InventoryService] Reducing stock for Laptop // [AnalyticsService] Recording order event
class EventBus { constructor() { this._subscribers = {}; } subscribe(event, callback) { if (!this._subscribers[event]) { this._subscribers[event] = []; } this._subscribers[event].push(callback); } unsubscribe(event, callback) { if (this._subscribers[event]) { this._subscribers[event] = this._subscribers[event].filter( (cb) => cb !== callback ); } } publish(event, data) { (this._subscribers[event] || []).forEach((cb) => cb(data)); } } const bus = new EventBus(); const onUserCreated = (user) => console.log(`Welcome email sent to ${user.email}`); bus.subscribe('userCreated', onUserCreated); bus.subscribe('userCreated', (user) => console.log(`Audit log: new user ${user.id}`)); bus.publish('userCreated', { id: 1, email: '[email protected]' }); // Welcome email sent to [email protected] // Audit log: new user 1 bus.unsubscribe('userCreated', onUserCreated); bus.publish('userCreated', { id: 2, email: '[email protected]' }); // Audit log: new user 2
Common Use Cases:
- Authentication strategies (JWT, OAuth2, API Key, Session) — Passport.js is built entirely on this pattern
- Payment processing (Stripe, PayPal, Braintree) selectable at checkout
- Data compression strategies (gzip, brotli, deflate) chosen by client
Accept-Encodingheader - Caching backends (in-memory, Redis, Memcached) swapped by environment
- Sorting or pagination algorithms that vary by dataset size or user preference
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from the clients that use it.
// strategies/sortStrategies.js const bubbleSort = (arr) => { const a = [...arr]; for (let i = 0; i < a.length; i++) for (let j = 0; j < a.length - i - 1; j++) if (a[j] > a[j + 1]) [a[j], a[j + 1]] = [a[j + 1], a[j]]; return a; }; const quickSort = (arr) => { if (arr.length <= 1) return arr; const pivot = arr[Math.floor(arr.length / 2)]; const left = arr.filter((x) => x < pivot); const mid = arr.filter((x) => x === pivot); const right = arr.filter((x) => x > pivot); return [...quickSort(left), ...mid, ...quickSort(right)]; }; class Sorter { constructor(strategy) { this.strategy = strategy; } setStrategy(strategy) { this.strategy = strategy; } sort(data) { return this.strategy(data); } } const sorter = new Sorter(bubbleSort); console.log(sorter.sort([5, 3, 1, 4, 2])); // [1, 2, 3, 4, 5] sorter.setStrategy(quickSort); console.log(sorter.sort([5, 3, 1, 4, 2])); // [1, 2, 3, 4, 5]
// authStrategies.js const jwtStrategy = async (req) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) throw new Error('No token provided'); // verify token (pseudo-code) return { userId: 1, role: 'admin' }; }; const apiKeyStrategy = async (req) => { const apiKey = req.headers['x-api-key']; if (!apiKey || apiKey !== process.env.API_KEY) throw new Error('Invalid API key'); return { userId: 99, role: 'service' }; }; class AuthContext { constructor(strategy) { this._strategy = strategy; } setStrategy(strategy) { this._strategy = strategy; } async authenticate(req) { return this._strategy(req); } } module.exports = { AuthContext, jwtStrategy, apiKeyStrategy };
Common Use Cases:
- Request logging and tracing (Morgan, Winston middleware)
- Authentication and authorization guards (JWT verification, role checks)
- Input validation and sanitization (
express-validator,Joi) - Rate limiting and throttling (
express-rate-limit) - CORS handling (
corspackage) - Global error handling (centralized
(err, req, res, next)handler) - Response compression (
compressionmiddleware)
The Middleware pattern is a pipeline of functions where each function can process a request, optionally pass it to the next function, or terminate the chain. Express.js is built entirely around this pattern.
const express = require('express'); const app = express(); // Logger middleware const logger = (req, res, next) => { console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`); next(); }; // Auth middleware const auth = (req, res, next) => { const token = req.headers.authorization; if (!token) return res.status(401).json({ error: 'Unauthorized' }); req.user = { id: 1 }; // decoded from token next(); }; // Error-handling middleware (4 arguments) const errorHandler = (err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: err.message }); }; app.use(logger); app.use(express.json()); app.get('/profile', auth, (req, res) => { res.json({ user: req.user }); }); app.use(errorHandler); app.listen(3000);
class Pipeline { constructor() { this._middlewares = []; } use(fn) { this._middlewares.push(fn); return this; // enables chaining } async execute(context) { const runner = async (index) => { if (index >= this._middlewares.length) return; const middleware = this._middlewares[index]; await middleware(context, () => runner(index + 1)); }; await runner(0); } } // Usage const pipeline = new Pipeline(); pipeline .use(async (ctx, next) => { console.log('Middleware 1: before'); await next(); console.log('Middleware 1: after'); }) .use(async (ctx, next) => { console.log('Middleware 2: processing'); ctx.result = 'Done'; await next(); }); pipeline.execute({}).then(() => console.log('Pipeline complete')); // Middleware 1: before // Middleware 2: processing // Middleware 1: after // Pipeline complete
Common Use Cases:
- Wrapping service methods with caching without modifying the service class
- Adding logging or performance metrics around repository calls
- Applying retry logic around HTTP client requests
- Enforcing authorization checks on service layer methods
- Instrumenting functions with distributed tracing spans (OpenTelemetry)
- NestJS
@UseGuards(),@UseInterceptors(),@UsePipes()are decorator-based
The Decorator pattern attaches additional responsibilities to an object dynamically, providing a flexible alternative to subclassing.
// base service class UserService { getUser(id) { return { id, name: 'Alice', email: '[email protected]' }; } } // Logging Decorator class LoggingDecorator { constructor(service) { this._service = service; } getUser(id) { console.log(`[LOG] getUser called with id=${id}`); const result = this._service.getUser(id); console.log(`[LOG] getUser returned:`, result); return result; } } // Caching Decorator class CachingDecorator { constructor(service) { this._service = service; this._cache = new Map(); } getUser(id) { if (this._cache.has(id)) { console.log(`[CACHE] Hit for id=${id}`); return this._cache.get(id); } const result = this._service.getUser(id); this._cache.set(id, result); return result; } } // Stack decorators const service = new CachingDecorator(new LoggingDecorator(new UserService())); service.getUser(1); // LOG + fetch service.getUser(1); // CACHE hit
// TypeScript with experimentalDecorators: true function log(target: any, key: string, descriptor: PropertyDescriptor) { const original = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`Calling ${key} with`, args); const result = original.apply(this, args); console.log(`${key} returned`, result); return result; }; return descriptor; } class MathService { @log add(a: number, b: number): number { return a + b; } } const svc = new MathService(); svc.add(2, 3); // Calling add with [2, 3] // add returned 5
Common Use Cases:
- Input validation on model objects before persisting to a database
- Lazy initialization of expensive resources (DB connections, ML model loading)
- Read-only enforcement on shared configuration objects
- Reactive state tracking — Vue 3's reactivity system is built on
Proxy - API mocking and test spies that transparently record calls
- Transparent request forwarding in API gateway or reverse proxy services
The Proxy pattern provides a surrogate to control access to another object. ES6 Proxy enables this natively.
const validator = { set(target, prop, value) { if (prop === 'age') { if (typeof value !== 'number' || value < 0 || value > 120) throw new RangeError('Age must be a number between 0 and 120'); } if (prop === 'email') { if (!/^[\w.-]+@[\w.-]+\.\w+$/.test(value)) throw new TypeError('Invalid email address'); } target[prop] = value; return true; }, }; const user = new Proxy({}, validator); user.age = 25; // OK user.email = '[email protected]'; // OK try { user.age = -5; // RangeError: Age must be a number between 0 and 120 } catch (e) { console.error(e.message); }
function createLazyProxy(loader) { let instance = null; return new Proxy( {}, { get(target, prop) { if (!instance) { console.log('[Proxy] Initializing heavy resource...'); instance = loader(); } return instance[prop]; }, } ); } const heavyDB = createLazyProxy(() => ({ query: (sql) => `Result of: ${sql}`, })); // Resource not loaded yet console.log(heavyDB.query('SELECT 1')); // [Proxy] Initializing heavy resource... // Result of: SELECT 1 console.log(heavyDB.query('SELECT 2')); // Result of: SELECT 2 (no re-initialization)
Common Use Cases:
- Job/task queues (BullMQ, Bee-Queue) where each job is a serializable command object
- Undo/redo functionality in collaborative or document editing tools
- Database migrations — each migration has an
up()anddown()command - CLI command dispatchers (e.g.,
git commit,git resetmap to command objects) - CQRS architecture — write operations are modeled as explicit command objects
- Transactional outbox pattern — storing commands before executing them for reliability
The Command pattern encapsulates a request as an object, allowing parameterization, queuing, logging, and undoable operations.
class TextEditor { constructor() { this.content = ''; this._history = []; } executeCommand(command) { command.execute(this); this._history.push(command); } undo() { const command = this._history.pop(); if (command) command.undo(this); } } class InsertCommand { constructor(text) { this.text = text; } execute(editor) { editor.content += this.text; } undo(editor) { editor.content = editor.content.slice(0, -this.text.length); } } class DeleteCommand { constructor(count) { this.count = count; this._deleted = ''; } execute(editor) { this._deleted = editor.content.slice(-this.count); editor.content = editor.content.slice(0, -this.count); } undo(editor) { editor.content += this._deleted; } } const editor = new TextEditor(); editor.executeCommand(new InsertCommand('Hello')); editor.executeCommand(new InsertCommand(' World')); console.log(editor.content); // Hello World editor.executeCommand(new DeleteCommand(5)); console.log(editor.content); // Hello editor.undo(); console.log(editor.content); // Hello World
Common Use Cases:
- Encapsulating business logic with private helper functions not exposed to consumers
- Building npm packages with a controlled, minimal public API surface
- Isolating feature modules in large monolithic applications
- Separating concerns: routes, controllers, services, repositories each in their own module
- Creating utility libraries (date helpers, validators) with hidden implementation details
The Module pattern encapsulates related functionality and exposes only a public API, hiding internal details.
CommonJS (Node.js default):
// counterModule.js const counterModule = (() => { let _count = 0; // private return { increment() { _count++; }, decrement() { _count--; }, reset() { _count = 0; }, getCount() { return _count; }, }; })(); module.exports = counterModule;
ES Modules (.mjs or "type": "module" in package.json):
// counter.mjs let _count = 0; // module-scoped, not exported = private export const increment = () => _count++; export const decrement = () => _count--; export const reset = () => { _count = 0; }; export const getCount = () => _count;
// main.mjs import { increment, getCount } from './counter.mjs'; increment(); increment(); console.log(getCount()); // 2
// authModule.js const authModule = (() => { const _users = new Map(); function _hashPassword(password) { // simplified — use bcrypt in production return Buffer.from(password).toString('base64'); } function register(username, password) { _users.set(username, _hashPassword(password)); return true; } function login(username, password) { return _users.get(username) === _hashPassword(password); } // Reveal only public API return { register, login }; })(); module.exports = authModule; authModule.register('alice', 'secret'); console.log(authModule.login('alice', 'secret')); // true console.log(authModule.login('alice', 'wrong')); // false
Common Use Cases:
- Injecting database adapters into service layers so real DBs can be swapped with in-memory fakes in tests
- Providing mock email/SMS services during unit and integration testing
- NestJS — the entire framework is built on constructor-based DI with an IoC container
- Configuring different logger implementations per environment (console in dev, remote in prod)
- Swapping caching backends (in-memory vs Redis) via an injected cache interface
- Multi-tenant apps where different tenants use different storage or payment providers
DI is a technique where dependencies are provided to a class from outside rather than the class creating them itself. It promotes loose coupling and testability.
// Without DI — tightly coupled class OrderService { constructor() { this.db = require('./database'); // hard dependency } } // With DI — loosely coupled class OrderService { constructor(db, emailService, logger) { this.db = db; this.emailService = emailService; this.logger = logger; } async createOrder(order) { const result = await this.db.save('orders', order); await this.emailService.send(order.userEmail, 'Order Confirmed', `Order #${result.id}`); this.logger.log(`Order created: ${result.id}`); return result; } } // Compose dependencies at the entry point (app.js) const db = require('./adapters/mongoAdapter'); const emailService = require('./services/emailService'); const logger = require('./logger'); const orderService = new OrderService(db, emailService, logger);
class Container { constructor() { this._registry = new Map(); } register(name, factory) { this._registry.set(name, factory); } resolve(name) { const factory = this._registry.get(name); if (!factory) throw new Error(`Dependency not registered: ${name}`); return factory(this); } } // Register dependencies const container = new Container(); container.register('logger', () => ({ log: (msg) => console.log(`[LOG] ${msg}`), })); container.register('db', () => ({ save: async (col, doc) => ({ ...doc, id: Date.now() }), })); container.register('orderService', (c) => { const db = c.resolve('db'); const logger = c.resolve('logger'); return { async create(order) { const saved = await db.save('orders', order); logger.log(`Order ${saved.id} created`); return saved; }, }; }); // Resolve and use const orderService = container.resolve('orderService'); orderService.create({ item: 'Book', qty: 2 }); // [LOG] Order 1713340800000 created
// orderService.test.js (Jest) const OrderService = require('./orderService'); test('createOrder saves to db and logs', async () => { const mockDb = { save: jest.fn().mockResolvedValue({ id: 42, item: 'Phone' }), }; const mockEmail = { send: jest.fn().mockResolvedValue(true), }; const mockLogger = { log: jest.fn(), }; const service = new OrderService(mockDb, mockEmail, mockLogger); const result = await service.createOrder({ item: 'Phone', userEmail: '[email protected]' }); expect(mockDb.save).toHaveBeenCalledWith('orders', expect.objectContaining({ item: 'Phone' })); expect(mockEmail.send).toHaveBeenCalledWith('[email protected]', 'Order Confirmed', expect.any(String)); expect(mockLogger.log).toHaveBeenCalledWith(expect.stringContaining('42')); expect(result.id).toBe(42); });
Common Use Cases:
- Abstracting database access so service/business logic has no direct dependency on ORM or query syntax
- Swapping data sources (MongoDB → PostgreSQL) without touching service code
- Providing in-memory fake repositories for fast, isolated unit tests
- Centralizing query logic (pagination, filtering, sorting) in one place
- Multi-tenant apps where each tenant may use a different storage backend
The Repository pattern abstracts the data layer, exposing a collection-like interface to the domain/service layer. Services call repository methods (find, save, delete) and never write raw queries.
// repositories/userRepository.js class UserRepository { constructor(db) { this.db = db; // injected DB adapter (Mongoose model, pg pool, etc.) } async findById(id) { return this.db.findOne({ _id: id }); } async findByEmail(email) { return this.db.findOne({ email }); } async save(user) { return this.db.create(user); } async update(id, data) { return this.db.findOneAndUpdate({ _id: id }, data, { new: true }); } async delete(id) { return this.db.deleteOne({ _id: id }); } } module.exports = UserRepository;
// services/userService.js class UserService { constructor(userRepository) { this.userRepository = userRepository; } async getUserById(id) { const user = await this.userRepository.findById(id); if (!user) throw new Error(`User ${id} not found`); return user; } async registerUser(data) { const existing = await this.userRepository.findByEmail(data.email); if (existing) throw new Error('Email already registered'); return this.userRepository.save(data); } } module.exports = UserService;
// repositories/inMemoryUserRepository.js class InMemoryUserRepository { constructor() { this._store = new Map(); this._idSeq = 1; } async findById(id) { return this._store.get(id) || null; } async findByEmail(email) { for (const user of this._store.values()) { if (user.email === email) return user; } return null; } async save(data) { const user = { ...data, id: this._idSeq++ }; this._store.set(user.id, user); return user; } async update(id, data) { const user = { ...this._store.get(id), ...data }; this._store.set(id, user); return user; } async delete(id) { this._store.delete(id); } } module.exports = InMemoryUserRepository;
// userService.test.js const UserService = require('./services/userService'); const InMemoryUserRepository = require('./repositories/inMemoryUserRepository'); test('registerUser saves a new user', async () => { const repo = new InMemoryUserRepository(); const service = new UserService(repo); const user = await service.registerUser({ name: 'Alice', email: '[email protected]' }); expect(user.id).toBe(1); expect(user.email).toBe('[email protected]'); }); test('registerUser throws if email already exists', async () => { const repo = new InMemoryUserRepository(); const service = new UserService(repo); await service.registerUser({ name: 'Alice', email: '[email protected]' }); await expect( service.registerUser({ name: 'Alice2', email: '[email protected]' }) ).rejects.toThrow('Email already registered'); });
Common Use Cases:
- Composing behavior at runtime without deep inheritance chains
- Delegating I/O, logging, or notification tasks from a primary class to a helper
- Forwarding method calls in proxy/wrapper classes
- Implementing role-based behavior (an object delegates work based on the caller's role)
- Node.js
stream.pipelinedelegates data handling across multiple transform streams
The Delegate pattern allows an object to hand off (delegate) responsibilities to a helper object instead of implementing them directly. It favors composition over inheritance.
// delegates/emailDelegate.js class EmailDelegate { send(to, subject, body) { console.log(`[Email] To: ${to} | Subject: ${subject} | Body: ${body}`); } } // delegates/smsDelegate.js class SMSDelegate { send(to, message) { console.log(`[SMS] To: ${to} | Message: ${message}`); } } // NotificationService delegates to specific senders class NotificationService { constructor(emailDelegate, smsDelegate) { this.emailDelegate = emailDelegate; this.smsDelegate = smsDelegate; } notifyByEmail(user, subject, body) { this.emailDelegate.send(user.email, subject, body); } notifyBySMS(user, message) { this.smsDelegate.send(user.phone, message); } notifyAll(user, subject, body) { this.notifyByEmail(user, subject, body); this.notifyBySMS(user, body); } } const service = new NotificationService(new EmailDelegate(), new SMSDelegate()); service.notifyAll( { email: '[email protected]', phone: '+1234567890' }, 'Welcome', 'Thanks for signing up!' ); // [Email] To: [email protected] | Subject: Welcome | Body: Thanks for signing up! // [SMS] To: +1234567890 | Message: Thanks for signing up!
| Inheritance | Delegation | |
|---|---|---|
| Coupling | Tight — child depends on parent internals | Loose — delegate is swappable |
| Reuse | Vertical (extend a class) | Horizontal (compose any object) |
| Runtime flexibility | Fixed at class definition | Delegate can be replaced at runtime |
| Testing | Harder to isolate | Delegate can be mocked independently |
// Inheritance approach — tightly coupled class BaseLogger { log(msg) { console.log(`[BASE] ${msg}`); } } class AppService extends BaseLogger { doWork() { this.log('Working...'); // directly calls inherited method } } // Delegation approach — loosely coupled class AppService { constructor(logger) { this.logger = logger; // any object with .log() } doWork() { this.logger.log('Working...'); } } const service = new AppService({ log: (msg) => console.log(`[CUSTOM] ${msg}`) }); service.doWork(); // [CUSTOM] Working...
Common Use Cases:
- Chat room where users communicate via a central mediator, not directly with each other
- Air traffic control — planes communicate through the tower, not peer-to-peer
- Event bus / message broker decoupling microservices (similar to pub/sub but with routing logic)
- Form wizard where steps communicate through a central coordinator
- Express router acting as mediator between incoming requests and route handlers
The Mediator pattern defines an object that encapsulates how a set of objects interact, promoting loose coupling by preventing objects from referring to each other explicitly.
// chatMediator.js class ChatMediator { constructor() { this._users = []; } register(user) { this._users.push(user); user.mediator = this; } send(message, sender) { this._users.forEach((user) => { if (user !== sender) { user.receive(message, sender.name); } }); } } class ChatUser { constructor(name) { this.name = name; this.mediator = null; } send(message) { console.log(`${this.name} sends: "${message}"`); this.mediator.send(message, this); } receive(message, from) { console.log(`${this.name} received from ${from}: "${message}"`); } } const mediator = new ChatMediator(); const alice = new ChatUser('Alice'); const bob = new ChatUser('Bob'); const carol = new ChatUser('Carol'); mediator.register(alice); mediator.register(bob); mediator.register(carol); alice.send('Hello everyone!'); // Alice sends: "Hello everyone!" // Bob received from Alice: "Hello everyone!" // Carol received from Alice: "Hello everyone!"
const EventEmitter = require('events'); class Mediator extends EventEmitter { constructor() { super(); this._handlers = {}; } subscribe(event, handler) { if (!this._handlers[event]) this._handlers[event] = []; this._handlers[event].push(handler); this.on(event, handler); } dispatch(event, data) { this.emit(event, data); } } // Components that communicate only through the mediator class InventoryService { constructor(mediator) { mediator.subscribe('orderPlaced', ({ item, qty }) => { console.log(`[Inventory] Reducing stock: ${qty}x ${item}`); mediator.dispatch('stockUpdated', { item, remaining: 10 - qty }); }); } } class ShippingService { constructor(mediator) { mediator.subscribe('orderPlaced', ({ orderId, address }) => { console.log(`[Shipping] Scheduling delivery for order ${orderId} to ${address}`); }); mediator.subscribe('stockUpdated', ({ item, remaining }) => { console.log(`[Shipping] Stock update received: ${item} has ${remaining} left`); }); } } const mediator = new Mediator(); new InventoryService(mediator); new ShippingService(mediator); mediator.dispatch('orderPlaced', { orderId: 1, item: 'Laptop', qty: 2, address: '123 Main St' }); // [Inventory] Reducing stock: 2x Laptop // [Shipping] Scheduling delivery for order 1 to 123 Main St // [Shipping] Stock update received: Laptop has 8 left