DEV Community

Cover image for JavaScript Design Patterns — And When You Should Actually Use Them
Anisubhra Sarkar (Ani)
Anisubhra Sarkar (Ani)

Posted on

JavaScript Design Patterns — And When You Should Actually Use Them

JavaScript applications grow fast — more features, more modules, more complexity. Without structure, even senior developers end up drowning in tech debt.

Design patterns help you:

✔ Organize code
✔ Avoid duplication
✔ Improve maintainability
✔ Scale features safely
✔ Build predictable architecture

But not all classical design patterns make sense in JavaScript.
This article focuses only on patterns that are truly relevant to JS and used in real-world frontend development.


1. Singleton Pattern

What It Is

A pattern that ensures only one instance of an object exists.

Example

class AuthService {
  constructor() {
    if (AuthService.instance) return AuthService.instance;
    this.user = null;
    AuthService.instance = this;
  }
}
Enter fullscreen mode Exit fullscreen mode

When to Use

  • Auth manager
  • Theme manager
  • App configuration
  • Global caching layer

Real-World Use

Your React/Angular app shouldn’t create multiple authentication handlers.
A singleton ensures global consistency.


2. Module Pattern

What It Is

Encapsulates private logic and exposes only what’s needed.

Example

const Cart = (() => {
  let items = [];
  function add(item) { items.push(item); }
  function get() { return items; }
  return { add, get };
})();
Enter fullscreen mode Exit fullscreen mode

When to Use

  • Utility helpers
  • Data services
  • Local storage wrappers
  • Analytics modules

Real-World Use

Your storageService shouldn't expose internals like keys or prefixes — module pattern keeps everything safe and clean.


3. Observer Pattern

What It Is

Allows one object to notify multiple listeners when something changes.

Example

class Observable {
  constructor() { this.subscribers = []; }
  subscribe(fn) { this.subscribers.push(fn); }
  notify(value) { this.subscribers.forEach(fn => fn(value)); }
}
Enter fullscreen mode Exit fullscreen mode

When to Use

  • Event systems
  • UI updates
  • Real-time data
  • Pub-sub based apps

Real-World Use

  • UI state updates in vanilla JS
  • Notifications
  • WebSocket live updates
  • Custom event emitters in React, Vue, or Node.js

4. Factory Pattern

What It Is

Creates objects without exposing creation logic.

Example

function createUser(type) {
  if (type === "admin") return { role: "admin", canDelete: true };
  return { role: "user", canDelete: false };
}
Enter fullscreen mode Exit fullscreen mode

When to Use

  • Conditional object creation
  • Multiple API versions
  • Data adapters

Real-World Use

Payment integrations: Stripe, Paypal, Razorpay may return different shapes — a factory normalizes them.


5. Strategy Pattern

What It Is

Defines interchangeable algorithms and selects one dynamically.

Example

const feeStrategies = {
  credit: amt => amt * 0.02,
  upi: amt => amt * 0,
  debit: amt => amt * 0.01
};

function calculateFee(method, amount) {
  return feeStrategies[method](amount);
}
Enter fullscreen mode Exit fullscreen mode

When to Use

  • Dynamic business logic
  • Replace long if-else chains
  • Pricing or discount calculations

Real-World Use

  • Payment fee calculation
  • Sorting functions for lists
  • Feature toggles

6. Decorator Pattern

What It Is

Wraps a function or object to extend behavior without modifying input code.

Example

function withLog(fn) {
  return (...args) => {
    console.log("Running", fn.name);
    return fn(...args);
  };
}
Enter fullscreen mode Exit fullscreen mode

When to Use

  • Logging
  • Caching
  • Throttling / Debouncing
  • Authorization wrappers

Real-World Use

  • React higher-order components (withAuth)
  • Axios interceptors
  • Performance measurement wrappers

7. Proxy Pattern

What It Is

Intercepts interactions with an object.

Example

const user = { name: "Ani" };

const proxy = new Proxy(user, {
  get(target, prop) {
    console.log("Access:", prop);
    return target[prop];
  }
});
Enter fullscreen mode Exit fullscreen mode

When to Use

  • Validation
  • Access control
  • Reactivity
  • Memoization

Real-World Use

Vue 3’s entire reactivity system is built with Proxy.


8. Command Pattern

What It Is

Encapsulates actions as objects (execute + undo).

Example

class Command {
  constructor(execute, undo) {
    this.execute = execute;
    this.undo = undo;
  }
}
Enter fullscreen mode Exit fullscreen mode

When to Use

  • Undo/redo functionality
  • History tracking
  • Macro actions

Real-World Use

  • Figma undo stack
  • VS Code command palette
  • Browser-like navigation history

9. Adapter Pattern

What It Is

Converts an incompatible interface to a usable one.

Example

function axiosToFetch(response) {
  return {
    status: response.status,
    data: response.data
  };
}
Enter fullscreen mode Exit fullscreen mode

When to Use

  • Migrating codebases
  • Integrating third-party APIs
  • Normalizing inconsistent data

Real-World Use

Converting axios responses into a unified format so your API layer works with both Fetch and Axios.


10. Mediator Pattern

What It Is

The Mediator pattern introduces a central controller that manages communication between multiple objects or components, preventing them from directly referencing each other.

Instead of components talking to each other directly (tight coupling), they communicate through the mediator.

Example

class UIEventMediator {
  constructor() {
    this.components = {};
  }

  register(name, component) {
    this.components[name] = component;
    component.setMediator(this);
  }

  notify(event, data) {
    switch (event) {
      case "USER_LOGGED_IN":
        this.components.navbar.updateUser(data);
        this.components.sidebar.showMenu();
        break;

      case "USER_LOGGED_OUT":
        this.components.navbar.clearUser();
        this.components.sidebar.hideMenu();
        break;
    }
  }
}

// Components
class Navbar {
  setMediator(mediator) {
    this.mediator = mediator;
  }

  updateUser(user) {
    console.log("Navbar updated with user:", user.name);
  }

  clearUser() {
    console.log("Navbar cleared");
  }
}

class Sidebar {
  setMediator(mediator) {
    this.mediator = mediator;
  }

  showMenu() {
    console.log("Sidebar menu shown");
  }

  hideMenu() {
    console.log("Sidebar menu hidden");
  }
}

// Usage
const mediator = new UIEventMediator();

const navbar = new Navbar();
const sidebar = new Sidebar();

mediator.register("navbar", navbar);
mediator.register("sidebar", sidebar);

// Trigger event
mediator.notify("USER_LOGGED_IN", { name: "Ani" });
Enter fullscreen mode Exit fullscreen mode

When to Use

  • Multiple UI components depend on the same events
  • Components should not directly reference each other
  • Communication logic is getting scattered
  • You want a single source of truth for interactions

Real-World Use Cases

  • Modal systems (open/close affecting multiple components)
  • Authentication flows (login affects navbar, sidebar, permissions)
  • Dashboards where widgets react to global events
  • Complex forms with dependent fields
  • Micro-frontend communication layer

Real-Life Use Cases for These Patterns

Here’s how these patterns map to daily frontend work:

Scenario Pattern Why
Global auth / theme / config Singleton Shared state consistency
API services, storage utils Module Encapsulation + maintainability
UI reacting to data changes Observer One-to-many updates
Dynamic object creation Factory Clean, scalable creation logic
Switchable business rules Strategy Removes if-else chains
Wrapping logic with caching/logging Decorator Non-intrusive behavior
Input validation or state tracking Proxy Fine-grained control
Undo/Redo UI interactions Command Action history
Integrating external tools Adapter Makes different APIs work
Coordinating multiple UI components Mediator Centralized, decoupled communication

Final Thoughts

Design patterns aren’t magic. They’re tools to make your JavaScript cleaner, scalable, testable, and more maintainable.

Use them when they solve a real problem — not because they're “best practice.”

Top comments (0)