JavaScript Design Patterns with Code Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Design Patterns in JavaScript with Examples



Design patterns are proven solutions to typical problems in software development. For a JavaScript developer, knowledge of patterns is especially important, as the language has a flexible object model and is widely used both on the client and server side (Node.js). In this article, we will look at four key patterns with practical examples in JavaScript.



Why study design patterns?

Patterns help:

  • Simplify code maintenance and scaling
  • Make code more readable and predictable
  • Avoid "reinventing the wheel"
  • Improve team communication (common language)


1. Singleton

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. In Node.js, this pattern is often used for database connections, configuration management, or logging.



class DatabaseConnection {  constructor() {    if (DatabaseConnection.instance) {      return DatabaseConnection.instance;    }    this.connection = null;    DatabaseConnection.instance = this;  }

connect() { // Simulating a DB connection this.connection = "connected"; console.log("Database connection established"); }

query(sql) { if (!this.connection) { throw new Error("No database connection"); } console.log(`Executing query: ${sql}`); return { data: [] }; }}

// Usageconst db1 = new DatabaseConnection();const db2 = new DatabaseConnection();

db1.connect();console.log(db1 === db2); // true - both point to the same instancedb2.query("SELECT * FROM users");


When to use: managing shared resources (DB, configuration, cache). Caution: excessive use can lead to tight code coupling.



2. Factory

The Factory Method defines an interface for creating an object but allows subclasses to alter the type of object that will be created. In JavaScript, this is often implemented through regular functions or classes.



class User {  constructor(name, role) {    this.name = name;    this.role = role;  }

getPermissions() { return []; }}

class Admin extends User { getPermissions() { return ["read", "write", "delete", "manage"]; }}

class Editor extends User { getPermissions() { return ["read", "write"]; }}

class Viewer extends User { getPermissions() { return ["read"]; }}

// Factoryclass UserFactory { createUser(name, role) { switch (role) { case "admin": return new Admin(name, role); case "editor": return new Editor(name, role); case "viewer": return new Viewer(name, role); default: throw new Error("Unknown role"); } }}

// Usageconst factory = new UserFactory();const admin = factory.createUser("Anna", "admin");const viewer = factory.createUser("Ivan", "viewer");

console.log(admin.getPermissions()); // ["read", "write", "delete", "manage"]console.log(viewer.getPermissions()); // ["read"]


Advantages: centralized object creation management, ease of adding new types, hiding instantiation logic.



3. Observer

The Observer pattern defines a one-to-many dependency between objects. When one object (the subject) changes its state, all dependent objects (observers) are automatically notified. This is the foundation of event-driven programming in Node.js.



class EventEmitter {  constructor() {    this.events = {};  }

on(eventName, listener) { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push(listener); }

emit(eventName, data) { const listeners = this.events[eventName]; if (listeners) { listeners.forEach(listener => listener(data)); } }

off(eventName, listenerToRemove) { const listeners = this.events[eventName]; if (listeners) { this.events[eventName] = listeners.filter( listener => listener !== listenerToRemove ); } }}

// Usageconst emitter = new EventEmitter();

function onUserLoggedIn(user) { console.log(`User logged in: ${user.name}`);}

emitter.on("userLogin", onUserLoggedIn);emitter.emit("userLogin", { name: "Maria" });// User logged in: Maria

emitter.off("userLogin", onUserLoggedIn);


When to use: event handling, notification systems, updating UI components when data changes.

Blogs

Book Recommendations