Suggest an editImprove this articleRefine the answer for “What are the most common design patterns in Node.js?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Design patterns in Node.js** are reusable solutions built around the event loop and non-blocking I/O. The most common ones: Module (`module.exports` closure), Singleton (`require()` cache), Observer (`EventEmitter`), Middleware (`next()` chaining), Factory (dynamic object creation), and Promise Chain for async flows. ```js // Every module is singleton-like automatically module.exports = { inc: () => ++count, get: () => count }; const c1 = require('./counter'); const c2 = require('./counter'); // c1 === c2: true ``` **Key point:** `require()` caches module exports, so shared state is singleton by default without any extra code.Shown above the full answer for quick recall.Answer (EN)Image**Design patterns in Node.js** are reusable solutions shaped around the event loop, non-blocking I/O, and `require()` caching. The six most common ones in production codebases are Module, Singleton, Observer/PubSub, Middleware, Factory, and Promise Chain. ## Theory ### TL;DR - Node.js patterns adapt classic ideas to async callbacks, streams, and the event loop. Think: modules are kitchen stations (one job each), singletons are the shared chef's knife, pubsub is the expediter calling orders across the kitchen. - Main difference from Java or C#: these patterns use `require()` caching, `EventEmitter`, and `async/await` instead of class hierarchies and blocking constructors. - Rule: if more than 3 components need to interact, use PubSub instead of direct calls. - Node caches every `require()` result in `Module._cache`, so any exported object is singleton-like for free. - Middleware is just function chaining via `next()` - that's the entire mental model for Express pipelines. ### Quick example ```js // Module pattern - natural in Node.js (counter.js) let count = 0; // Private - nothing outside can read or mutate this directly module.exports = { inc: () => ++count, get: () => count }; // app.js const counter = require('./counter'); counter.inc(); console.log(counter.get()); // 1 console.log(counter.count); // undefined - encapsulation holds ``` Node caches the module on first `require()`, so `count` persists across all files that import this. That's the Module and Singleton combo in 8 lines, no class required. ### Why Node.js patterns look different from OOP languages Classic GoF patterns assume you create objects through constructors and manage their lifecycle manually. Node skips most of that. The module cache handles singleton behavior automatically. `EventEmitter` covers pub/sub without a dedicated bus library. And `async/await` replaces the complex threading those patterns were originally designed to tame. So instead of a Singleton class with a private constructor and a static `getInstance()` method, you just export an instance. That's it. The pattern is still there - it just wears different clothes. ### When to use which pattern - **Encapsulate private state** - Module pattern. Default for every file you write. - **Share one instance across the app** - Singleton. DB connections, loggers, config objects. - **Decouple event producers from consumers** - Observer/PubSub. Order processing, WebSockets, user events. - **Chain request processing** - Middleware. Express routes, auth, logging, rate limiting. - **Create objects without hardcoding a class** - Factory. DB adapters, plugins, config-driven types. - **Handle sequential async steps** - Promise Chain or async iterators. API pipelines, data transforms. ### Comparison table | Pattern | Core mechanism | Async fit | Real library | When to use | |---------|---------------|-----------|-------------|-------------| | Module | `module.exports` + closure | Both | Built-in | Always, every file | | Singleton | `require()` cache | Both | Winston logger | One global resource | | Observer/PubSub | `EventEmitter` | Async | Socket.io | Event streams | | Middleware | `next()` function chain | Async | Express | HTTP pipelines | | Factory | Function returning new instances | Both | Mongoose models | Dynamic types | | Promise Chain | `.then()` / `async/await` | Async | Axios | Data flows | ### How Node.js handles this internally V8 compiles a module the first time `require()` is called, then stores the result in `Module._cache` - a C++ hash table. Every subsequent `require()` of the same path returns the cached object without re-executing the file. That's free singleton behavior, built into the runtime. `EventEmitter` uses libuv's epoll/kqueue for non-blocking event queues. When you call `.emit()`, it fires registered callbacks from the event loop without spawning threads or blocking. Express's middleware stack runs via `next()` recursion inside its Router, yielding control back to libuv between each step. ### Common mistakes **Mutating shared module state and expecting per-import isolation:** ```js // Wrong - all importers share this same cache object let cache = {}; module.exports = { set: (key, val) => { cache[key] = val; }, get: (key) => cache[key] }; // One test sets cache['user'] = 'Alice', next test sees it too ``` Node caches the module once. Every file that imports this shares the same `cache`. If isolation matters, return a factory function that creates a fresh closure per call. **Forgetting to remove EventEmitter listeners:** ```js // Wrong - handler stays in memory after socket closes socket.on('data', handler); socket.end(); // Right const handler = (data) => processData(data); socket.on('data', handler); socket.on('close', () => socket.removeListener('data', handler)); ``` Lingering listeners are the most common cause of memory leaks in Node apps. Node warns you when a single emitter exceeds 10 listeners by default - that warning is not noise, it's a signal. **Assuming singleton works across a cluster:** ```js // Works in a single process, breaks in PM2 cluster mode if (Database.instance) return Database.instance; Database.instance = new Database(); ``` Each worker process has its own `Module._cache`. Singleton is per-process, not per-cluster. For truly shared state across workers, use IPC via the `cluster` module or an external store like Redis. **Middleware without error propagation:** ```js // Wrong - unhandled throw hangs the request app.use((req, res, next) => { parseBody(req); // throws on bad JSON next(); }); // Right app.use((req, res, next) => { try { parseBody(req); next(); } catch (e) { next(e); // Express error handler takes it from here } }); ``` **Factory that secretly returns the same instance:** ```js // This is a singleton, not a factory const getDB = () => db; // Same object every call // Actual factory - new instance per call const createDB = (config) => new DatabaseAdapter(config); ``` If your factory always returns the same object, it's a singleton with extra steps. Factories produce new instances - that's the definition. ### Real-world usage - **Express** - Middleware for auth, logging, and rate limiting in almost every production Node app. - **Socket.io** - PubSub via `EventEmitter` for real-time features: chat, live dashboards, collaborative tools. - **Winston** - Singleton logger created once and imported everywhere via the `require()` cache. - **Mongoose** - Factory for dynamic models: `mongoose.model('User', schema)` returns a new constructor based on config. - **Node Redis** - Singleton client with built-in PubSub channels for distributed messaging across processes. I've seen codebases where teams didn't use any patterns intentionally and ended up with accidental singletons (shared mutable state), accidental middleware (functions passing callbacks), and accidental factories (functions returning different object shapes). Naming and recognizing patterns is what separates maintainable code from a pile of functions that happen to work. ### Follow-up questions **Q:** How does Node's module cache make every module a singleton? **A:** First `require()` executes the file and stores exports in `Module._cache`. All later `require()` calls return that cached value without re-running the file. One execution, one object, per process. **Q:** What's the difference between `EventEmitter` PubSub and Redis PubSub? **A:** `EventEmitter` is in-memory and works only within one Node process. Redis PubSub is distributed - messages reach subscribers across multiple servers or processes, and the broker persists outside your app. **Q:** How do you implement middleware that times out after 5 seconds? **A:** Set a timer and clear it when the response finishes: ```js const timeout = (ms) => (req, res, next) => { const timer = setTimeout(() => next(new Error('Request timeout')), ms); res.on('finish', () => clearTimeout(timer)); next(); }; app.use(timeout(5000)); ``` **Q:** In a PM2 cluster, how do you share a singleton connection across workers? **A:** The master process creates the connection and passes messages to workers via `cluster.worker.send()` and `process.on('message')`. Or use an external connection pool like `pg-pool` that manages per-process connections. **Q:** What happens if you delete a module from `require.cache`? **A:** The next `require()` re-executes the file and creates a fresh instance. Your singleton is gone. Hot-reload tools use this intentionally - it's also a common source of confusion in tests that clear the cache between runs. **Q:** Why avoid class-based singletons in Node.js? **A:** `require()` caching handles it already. A class with a private constructor adds boilerplate without any real benefit. `module.exports = new MyService()` does the same thing in one line and reads more clearly. ## Examples ### Module and Singleton: shared logger across the app ```js // logger.js - one instance for the entire app const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.simple(), transports: [new winston.transports.Console()] }); module.exports = logger; // Cached after first require // server.js const logger = require('./logger'); logger.info('Server started on port 3000'); // routes/users.js const logger = require('./logger'); // Same object, no new instance created logger.info('GET /users called'); ``` Both files share the exact same `logger`. No class, no static property - just the module cache. This is how Winston and most logger setups work in real projects. ### Middleware chain: logging and auth in Express ```js // middleware/index.js const logRequest = (req, res, next) => { console.log(`${new Date().toISOString()} ${req.method} ${req.url}`); next(); // Always pass to next handler }; const authenticate = (req, res, next) => { const token = req.headers.authorization; if (token === 'Bearer secret-token') { req.user = { id: 1, role: 'admin' }; next(); // Attach user and continue } else { res.status(401).json({ error: 'Unauthorized' }); // Chain stops here } }; module.exports = { logRequest, authenticate }; // app.js const express = require('express'); const { logRequest, authenticate } = require('./middleware'); const app = express(); app.use(logRequest); // Runs for every request app.use('/api', authenticate); // Runs for /api/* only app.get('/api/profile', (req, res) => { res.json({ user: req.user }); }); // GET /api/profile with correct token: // Logs: 2024-01-15T10:00:00.000Z GET /api/profile // Returns: { user: { id: 1, role: "admin" } } // Without token: 401 { error: 'Unauthorized' } ``` The chain stops at `authenticate` if the token is wrong. The route handler never runs. That's the Chain of Responsibility behavior - each step decides whether to continue or terminate. ### Observer pattern: order service with multiple subscribers ```js const EventEmitter = require('events'); class OrderService extends EventEmitter { async placeOrder(order) { const saved = { ...order, id: Date.now(), status: 'placed' }; this.emit('orderPlaced', saved); // Fire once, all handlers react return saved; } } const orderService = new OrderService(); // Three independent handlers - OrderService knows nothing about them orderService.on('orderPlaced', (order) => { console.log(`Email sent for order ${order.id}`); }); orderService.on('orderPlaced', (order) => { console.log(`Stock updated for: ${order.item}`); }); orderService.on('orderPlaced', (order) => { console.log(`Analytics: order ${order.id} tracked`); }); (async () => { await orderService.placeOrder({ item: 'Laptop', qty: 1 }); // Output: // Email sent for order 1705312800000 // Stock updated for: Laptop // Analytics: order 1705312800000 tracked })(); ``` Adding a fourth handler - say, a Slack notification - requires zero changes to `OrderService`. The producer emits, the consumers decide what to do with it. That decoupling is exactly why this pattern scales well when systems grow.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.