Node.js Design Patterns

πŸ“– Concept

Design patterns are proven solutions to recurring software design problems. In Node.js, certain patterns are especially important due to its asynchronous, event-driven nature.

Essential Node.js patterns:

Pattern Purpose Example
Singleton One instance across the app Database connection, logger
Factory Create objects without exposing logic Router creation, plugin loading
Observer React to state changes EventEmitter, Redis Pub/Sub
Strategy Swap algorithms at runtime Payment processors, auth strategies
Middleware Chain processing steps Express middleware, validation pipeline
Repository Abstract data access Database queries behind an interface
Dependency Injection Pass dependencies, don't import Testable services, modular code
Proxy Control access or add behavior Caching proxy, logging proxy

Module pattern in Node.js: Every file is a module β€” this is the foundation of encapsulation:

// Only exported functions are public
module.exports = { publicFunction };
// Internal helpers stay private to the module

Dependency Injection: Instead of importing dependencies directly (tight coupling), pass them as parameters (loose coupling):

// ❌ Tight coupling
const db = require('./database');
class UserService { findUser(id) { return db.query(...) } }

// βœ… Loose coupling (dependency injection)
class UserService {
  constructor(db) { this.db = db; }
  findUser(id) { return this.db.query(...) }
}

🏠 Real-world analogy: Design patterns are like cooking techniques (sautΓ©ing, braising, grilling). They're not recipes β€” they're reusable methods that experienced chefs (developers) apply to solve common cooking (coding) challenges. Knowing the right technique for the ingredient (problem) makes you a better chef.

πŸ’» Code Example

codeTap to expand β›Ά
1// Node.js Design Patterns
2
3// 1. Singleton β€” Database Connection
4class Database {
5 constructor() {
6 if (Database.instance) return Database.instance;
7 this.connection = null;
8 Database.instance = this;
9 }
10
11 async connect(uri) {
12 if (!this.connection) {
13 // this.connection = await mongoose.connect(uri);
14 this.connection = { uri, connected: true };
15 console.log("Database connected");
16 }
17 return this.connection;
18 }
19
20 static getInstance() {
21 if (!Database.instance) new Database();
22 return Database.instance;
23 }
24}
25
26// 2. Factory β€” Service Creator
27class ServiceFactory {
28 static create(type, config) {
29 switch (type) {
30 case "email":
31 return config.provider === "sendgrid"
32 ? new SendGridService(config.apiKey)
33 : new SMTPService(config.host, config.port);
34 case "storage":
35 return config.provider === "s3"
36 ? new S3Storage(config.bucket)
37 : new LocalStorage(config.path);
38 default:
39 throw new Error(`Unknown service type: ${type}`);
40 }
41 }
42}
43
44// 3. Strategy β€” Payment Processing
45class PaymentProcessor {
46 constructor(strategy) {
47 this.strategy = strategy;
48 }
49
50 setStrategy(strategy) {
51 this.strategy = strategy;
52 }
53
54 async processPayment(amount, details) {
55 return this.strategy.charge(amount, details);
56 }
57}
58
59class StripeStrategy {
60 async charge(amount, details) {
61 console.log(`Charging $${amount} via Stripe`);
62 return { provider: "stripe", transactionId: "txn_123" };
63 }
64}
65
66class PayPalStrategy {
67 async charge(amount, details) {
68 console.log(`Charging $${amount} via PayPal`);
69 return { provider: "paypal", transactionId: "pp_456" };
70 }
71}
72
73// Usage
74const processor = new PaymentProcessor(new StripeStrategy());
75// processor.processPayment(99.99, { card: "..." });
76// Switch strategy at runtime:
77// processor.setStrategy(new PayPalStrategy());
78
79// 4. Repository β€” Data Access Abstraction
80class UserRepository {
81 constructor(db) { this.db = db; }
82
83 async findById(id) {
84 // return this.db.collection("users").findOne({ _id: id });
85 return { id, name: "Alice" };
86 }
87
88 async findByEmail(email) {
89 // return this.db.collection("users").findOne({ email });
90 return null;
91 }
92
93 async create(userData) {
94 // const result = await this.db.collection("users").insertOne(userData);
95 return { id: Date.now(), ...userData };
96 }
97
98 async update(id, data) {
99 // return this.db.collection("users").updateOne({ _id: id }, { $set: data });
100 return { id, ...data };
101 }
102
103 async delete(id) {
104 // return this.db.collection("users").deleteOne({ _id: id });
105 return true;
106 }
107}
108
109// 5. Dependency Injection Container
110class Container {
111 constructor() {
112 this.services = new Map();
113 this.singletons = new Map();
114 }
115
116 register(name, factory, singleton = false) {
117 this.services.set(name, { factory, singleton });
118 }
119
120 resolve(name) {
121 const service = this.services.get(name);
122 if (!service) throw new Error(`Service "${name}" not registered`);
123
124 if (service.singleton) {
125 if (!this.singletons.has(name)) {
126 this.singletons.set(name, service.factory(this));
127 }
128 return this.singletons.get(name);
129 }
130
131 return service.factory(this);
132 }
133}
134
135// Usage
136const container = new Container();
137container.register("db", () => Database.getInstance(), true);
138container.register("userRepo", (c) => new UserRepository(c.resolve("db")), true);
139container.register("paymentProcessor", () => new PaymentProcessor(new StripeStrategy()));
140
141const userRepo = container.resolve("userRepo");
142const payments = container.resolve("paymentProcessor");
143
144// Mock classes for examples
145class SendGridService { constructor(key) { this.key = key; } }
146class SMTPService { constructor(host, port) { this.host = host; } }
147class S3Storage { constructor(bucket) { this.bucket = bucket; } }
148class LocalStorage { constructor(path) { this.path = path; } }
149
150module.exports = { Database, ServiceFactory, PaymentProcessor, UserRepository, Container };

πŸ‹οΈ Practice Exercise

Exercises:

  1. Implement the Singleton pattern for a database connection and a logger
  2. Build a Factory that creates different notification services (email, SMS, push) based on configuration
  3. Implement the Strategy pattern for a payment system with 3+ payment providers
  4. Create a Repository layer that abstracts database operations β€” swap between MongoDB and PostgreSQL
  5. Build a simple Dependency Injection container that supports registration, singleton, and resolution
  6. Refactor an Express app to use DI β€” all services receive dependencies via constructors

⚠️ Common Mistakes

  • Overusing the Singleton pattern β€” not everything should be a singleton; it makes testing harder because state is shared

  • Using design patterns for the sake of using them β€” patterns solve specific problems; don't add a Factory when a simple function will do

  • Not using Dependency Injection for services β€” direct imports create tight coupling and make unit testing impossible without module mocking

  • Mixing business logic with infrastructure β€” services should not know about Express, HTTP, or databases directly; use abstractions

  • Creating god objects/services β€” one class doing everything violates Single Responsibility; split into focused, composable classes

πŸ’Ό Interview Questions

🎀 Mock Interview

Mock interview is powered by AI for Node.js Design Patterns. Login to unlock this feature.