JavaScript Design Patterns: Building Scalable Applications

by Didin J. on Nov 30, 2025 JavaScript Design Patterns: Building Scalable Applications

Learn essential JavaScript design patterns with real-world examples to build clean, scalable, and maintainable frontend and backend applications.

JavaScript has grown from a simple scripting language into one of the most versatile technologies in modern software development. Today, it powers everything from small UI interactions to large-scale enterprise applications — both in the browser and on the server through Node.js.

As applications grow in size and complexity, maintaining clean, scalable, and predictable code becomes increasingly challenging. This is where design patterns come in.

What Are Design Patterns?

Design patterns are proven solutions to common problems in software design. They are not frameworks or libraries, but reusable templates that help you structure your code more effectively.

Examples:

  • Managing object creation efficiently

  • Handling communication between components

  • Simplifying complex interfaces

  • Improving maintainability and testability

Why Design Patterns Matter in JavaScript

JavaScript’s dynamic nature — functions as first-class citizens, closures, prototypal inheritance, async behavior — makes it uniquely flexible. However, this flexibility can also lead to messy, unstructured code when applications scale.

Using design patterns helps you:

  • Write cleaner, more modular code

  • Improve readability and maintainability

  • Avoid common pitfalls and anti-patterns

  • Enhance reusability across components

  • Build scalable applications with predictable behavior

When Should You Use Design Patterns?

Patterns are helpful when:

  • Your codebase is growing and becoming hard to maintain

  • Multiple parts of your system need to communicate frequently

  • You want to encapsulate complexity behind simple APIs

  • You need to enforce consistency across modules or features

Patterns should not be used to “force” complexity into simple code. The goal is to make software simpler, not more complicated.

What You’ll Learn in This Tutorial

This tutorial will break down the most practical and widely-used JavaScript design patterns with:

  • Clear explanations

  • Simple examples (ES6+ syntax)

  • Real-world use cases (React, Vue, Node.js, Express, Redux)

  • Best practices and anti-patterns to avoid

By the end, you’ll have a solid understanding of how to use design patterns to build scalable, maintainable, and production-ready JavaScript applications.


Understanding Design Patterns

Before diving into specific design patterns, it’s important to understand the concepts behind them. Design patterns are more than coding tricks — they represent proven approaches to solving recurring design challenges in software engineering.

What Are Design Patterns, Really?

Design patterns are general, reusable solutions for common software design problems. They are not copy-paste code but design blueprints that guide how you structure your functions, classes, and modules.

They help ensure that your application:

  • Scales without becoming chaotic

  • Remains maintainable as features grow

  • Has predictable behavior

  • Encourages solid coding principles like DRY, SOLID, and encapsulation

Three Main Categories of Design Patterns

Design patterns are typically grouped into three categories:

🔶 1. Creational Patterns

These patterns focus on how objects are created.
They help manage object creation to make systems more flexible and decoupled.

Examples:

  • Factory — creates objects without specifying the exact class

  • Singleton — ensures only one instance exists

  • Builder — constructs complex objects step by step

  • Prototype — creates new objects by cloning existing ones

Useful when:

  • Object creation is complex or repetitive

  • You want to decouple creation logic from usage

🔷 2. Structural Patterns

These patterns focus on how objects and components are composed.
Their goal is to simplify relationships and make your architecture easy to maintain.

Examples:

  • Module / Revealing Module — organize code with encapsulation

  • Decorator — add features to objects dynamically

  • Adapter — make incompatible interfaces work together

  • Facade — simplify complex systems with a single interface

  • Proxy — control access to another object (e.g., logging, caching)

Useful when:

  • You want cleaner architecture

  • You need to extend functionality without modifying the original code

  • Objects need to share or adapt features

🔵 3. Behavioral Patterns

These patterns focus on how objects interact and communicate.

Examples:

  • Observer / Pub-Sub — notify subscribers about changes

  • Strategy — switch between multiple algorithms dynamically

  • Command — represent actions as objects (useful in undo/redo)

  • Iterator — sequentially access items in a collection

  • Memento — snapshot and restore object state

Useful when:

  • Components need to communicate cleanly

  • You want interchangeable behaviors

  • You want decoupled and flexible logic

Why JavaScript Is Perfect for Design Patterns

JavaScript includes features that make implementing patterns simpler:

  • First-class functions

  • Closures

  • Prototypal inheritance

  • Classes (ES6+)

  • Modules

  • Event-driven architecture (Node.js)

This means patterns can be implemented elegantly with minimal boilerplate.

When NOT to Use a Pattern

Patterns become counterproductive when:

  • You add them just to “look advanced”

  • A simple function or module would work

  • They introduce unnecessary abstraction

The rule of thumb:
👉 Use a design pattern only when it makes the code more understandable or scalable.


Creational Patterns

Creational patterns help control how objects are created in JavaScript. Instead of instantiating objects directly in many places—causing duplicated logic and tight coupling—these patterns centralize, simplify, and manage the creation process.

Below are the most useful Creational Patterns in real-world JavaScript apps.

1. Factory Pattern

The Factory Pattern is used to create objects without exposing the creation logic to the calling code. Instead of using new everywhere, you use a function or class that returns the correct object type.

✔ When to Use

  • When you need different types of objects with similar structure

  • When object creation involves complex logic

  • When you want to centralize creation for consistency

Example: Simple Factory

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

class UserFactory {
  static createUser(name, role) {
    if (role === "admin") {
      return new User(name, "admin");
    }
    return new User(name, "user");
  }
}

const admin = UserFactory.createUser("John", "admin");
const user = UserFactory.createUser("Jane", "user");

console.log(admin, user);

Real-World Example

  • Creating different types of React components

  • Creating database models depending on the environment

  • Generating API clients dynamically

2. Singleton Pattern

The Singleton Pattern ensures only one instance of a class or module exists throughout the app.

JavaScript's module system naturally supports singletons.

✔ When to Use

  • Global configurations

  • Database connections (Node.js)

  • Logging utilities

  • Caches

Example: ES6 Singleton

class Config {
  constructor() {
    if (Config.instance) return Config.instance;
    this.settings = {};
    Config.instance = this;
  }
}

const config1 = new Config();
const config2 = new Config();

console.log(config1 === config2); // true

Using JS Modules (Easier Singleton)

// config.js
export default {
  appName: "MyApp",
  version: "1.0",
};

3. Builder Pattern

The Builder Pattern helps construct complex objects step-by-step, making code readable and flexible.

✔ When to Use

  • When an object has many optional fields

  • When you want a clean and fluent interface

  • When object creation gets messy with too many parameters

Example: Fluent Builder

class PizzaBuilder {
  constructor() {
    this.size = "medium";
    this.toppings = [];
  }

  setSize(size) {
    this.size = size;
    return this;
  }

  addTopping(topping) {
    this.toppings.push(topping);
    return this;
  }

  build() {
    return {
      size: this.size,
      toppings: this.toppings,
    };
  }
}

const pizza = new PizzaBuilder()
  .setSize("large")
  .addTopping("pepperoni")
  .addTopping("mushrooms")
  .build();

console.log(pizza);

Real-World Examples

  • Configuring HTTP requests (Axios)

  • Building UI forms

  • Constructing database queries

4. Prototype Pattern

The Prototype Pattern creates new objects by cloning existing ones, leveraging JavaScript’s native prototypal inheritance.

✔ When to Use

  • When cloning is cheaper than creating from scratch

  • When you want reusable base objects

  • When designing object templates with variations

Example: Object Cloning

const carPrototype = {
  drive() {
    console.log(`${this.make} ${this.model} is driving`);
  },
};

function createCar(make, model) {
  const car = Object.create(carPrototype);
  car.make = make;
  car.model = model;
  return car;
}

const car1 = createCar("Toyota", "Camry");
car1.drive(); 

Real-World Use

  • JavaScript's class inheritance underneath uses prototypes

  • Redux reducers often clone state using prototypes

  • DOM elements created via cloning templates

Summary of Creational Patterns

Creational patterns help you:

  • Avoid duplicated object creation logic

  • Improve flexibility and maintainability

  • Encapsulate complexity

  • Support scalable architecture

You now have clear examples and real-world use cases for each pattern.


Structural Patterns

Structural patterns help define how objects and components are organized. They focus on composition—building larger, more flexible structures from smaller pieces. These patterns simplify relationships and make your architecture easier to maintain and extend.

Below are the most practical Structural Patterns in modern JavaScript development.

1. Module Pattern

One of the most well-known patterns in JavaScript, used to encapsulate variables and functions and expose only what’s necessary.

✔ When to Use

  • To avoid polluting the global namespace

  • To create private variables and public APIs

  • To organize code logically

Example: Basic Module

const CartModule = (() => {
  let items = [];

  function add(item) {
    items.push(item);
  }

  function getItems() {
    return items;
  }

  return {
    add,
    getItems,
  };
})();

CartModule.add("Laptop");
console.log(CartModule.getItems());

Modern Replacement

👉 ES Modules (import/export) natively support modular code.

2. Revealing Module Pattern

Similar to the Module Pattern, but explicitly maps private functions to public ones. This keeps the code more readable.

Example

const UserModule = (() => {
  let users = [];

  function addUser(name) {
    users.push(name);
  }

  function getUserCount() {
    return users.length;
  }

  return {
    addUser,
    getUserCount,
  };
})();

More expressive and self-documenting than the basic module.

3. Adapter Pattern

Allows incompatible interfaces to work together. Useful when integrating third-party libraries or refactoring legacy code.

✔ When to Use

  • Replacing or upgrading libraries

  • Keeping old code compatible with new APIs

  • Providing a unified interface

Example

class OldAPI {
  getData() {
    return { name: "Old Data" };
  }
}

class NewAPI {
  fetch() {
    return { name: "New Data" };
  }
}

// Adapter makes NewAPI compatible with OldAPI interface
class APIAdapter {
  constructor() {
    this.api = new NewAPI();
  }

  getData() {
    return this.api.fetch();
  }
}

const api = new APIAdapter();
console.log(api.getData());

Real-World Usage

  • React adapting DOM events

  • Translating API schemas

  • Converting legacy callbacks to promises

4. Decorator Pattern

Adds additional behavior to objects without modifying their code. Widely used in frameworks like Angular, NestJS, and Express middleware.

✔ When to Use

  • To extend functionality dynamically

  • To add features like logging, caching, and validation

  • To keep classes and functions clean

Example: Function Decorator

function logger(fn) {
  return function (...args) {
    console.log(`Calling ${fn.name} with`, args);
    return fn(...args);
  };
}

function add(a, b) {
  return a + b;
}

const loggedAdd = logger(add);
console.log(loggedAdd(2, 3));

Real-World Examples

  • Express middleware

  • TypeScript class decorators

  • Vue and React higher-order components (HOCs)

5. Facade Pattern

Provides a simple interface to a complex system. Helps hide complexity and reduce dependencies.

✔ When to Use

  • When simplifying multiple API calls

  • When you want to group related operations

  • When exposing simpler interfaces to new developers

Example

class PaymentAPI {
  verify() { return true; }
  process() { return "processed"; }
}

class NotificationAPI {
  send() { return "notification sent"; }
}

// Facade
class OrderService {
  constructor() {
    this.payment = new PaymentAPI();
    this.notify = new NotificationAPI();
  }

  placeOrder() {
    if (this.payment.verify()) {
      this.payment.process();
      this.notify.send();
      return "Order completed!";
    }
  }
}

const order = new OrderService();
console.log(order.placeOrder());

Real-World Use Cases

  • Axios instance wrapper with default config

  • Firebase client libraries

  • Abstracting browser APIs

6. Proxy Pattern

Controls access to another object. Frequently used in caching, validation, logging, rate-limiting, and reactive UIs.

✔ When to Use

  • Intercepting property access

  • Lazy loading

  • Data validation

  • Creating reactive frameworks (Vue, MobX)

Example: Logging Proxy

const user = {
  name: "John",
};

const proxy = new Proxy(user, {
  get(target, property) {
    console.log(`Accessing ${property}`);
    return target[property];
  },
  set(target, property, value) {
    console.log(`Setting ${property} to ${value}`);
    target[property] = value;
    return true;
  }
});

proxy.name;         // logs read
proxy.age = 30;     // logs write

Real-World Usage

  • Vue 3 reactivity uses Proxy

  • Virtual Proxies for lazy loading images

  • Request throttling wrappers

Summary of Structural Patterns

Structural patterns help you:

  • Organize your code into scalable components

  • Simplify complex APIs

  • Extend functionality cleanly

  • Make architectures more maintainable


Behavioral Patterns

Behavioral patterns define how objects communicate and interact, helping structure logic flow in scalable and maintainable ways. These patterns are especially powerful in JavaScript because of its event-driven nature and flexible function handling.

Let’s explore the most widely used behavioral patterns in modern JavaScript applications.

1. Observer Pattern (Pub-Sub)

The Observer Pattern allows objects (subscribers) to be notified automatically when another object (subject) changes state.

JavaScript developers use this pattern constantly:

  • DOM events

  • Node.js EventEmitter

  • React state updates

  • Vue reactivity

  • Pub/Sub systems

✔ When to Use

  • Event-driven systems

  • Cross-component communication

  • Real-time apps

Example

class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(fn) {
    this.observers.push(fn);
  }

  unsubscribe(fn) {
    this.observers = this.observers.filter(sub => sub !== fn);
  }

  notify(data) {
    this.observers.forEach(fn => fn(data));
  }
}

const subject = new Subject();

const logger = (msg) => console.log("Logger:", msg);
subject.subscribe(logger);

subject.notify("Hello observers!");

2. Strategy Pattern

The Strategy Pattern enables selecting an algorithm at runtime. Instead of using large if/else or switch blocks, you can plug in interchangeable strategies.

✔ When to Use

  • Multiple ways to perform the same task

  • Payment processing methods

  • Form validation strategies

  • Sorting, filtering, and authentication flows

Example

const strategies = {
  sum: (a, b) => a + b,
  multiply: (a, b) => a * b,
};

function execute(strategy, a, b) {
  return strategies[strategy](a, b);
}

console.log(execute("sum", 2, 3));      // 5
console.log(execute("multiply", 2, 3)); // 6

3. Command Pattern

Encapsulates actions as objects. This helps with undo/redo, logging, batching, and task queues.

✔ When to Use

  • Undo/redo functionality

  • Task queues

  • Macro operations

  • Logging user actions

Example

class Command {
  constructor(execute, undo) {
    this.execute = execute;
    this.undo = undo;
  }
}

class Calculator {
  constructor() {
    this.value = 0;
    this.history = [];
  }

  run(command) {
    this.value = command.execute(this.value);
    this.history.push(command);
  }

  undo() {
    const command = this.history.pop();
    this.value = command.undo(this.value);
  }
}

const add = (x) => new Command(value => value + x, value => value - x);

const calc = new Calculator();
calc.run(add(10));
calc.run(add(5));
console.log(calc.value); // 15

calc.undo();
console.log(calc.value); // 10

4. Iterator Pattern

Provides a way to access elements of a collection without exposing its internal structure.

JavaScript has built-in iterator support:

  • Arrays

  • Maps

  • Sets

  • Generators

Example: Custom Iterator

class NameCollection {
  constructor(names) {
    this.names = names;
  }

  [Symbol.iterator]() {
    let index = 0;
    let names = this.names;

    return {
      next() {
        if (index < names.length) {
          return { value: names[index++], done: false };
        }
        return { done: true };
      }
    };
  }
}

const names = new NameCollection(["Alice", "Bob", "Charlie"]);
for (const name of names) {
  console.log(name);
}

5. Memento Pattern

Captures and restores an object’s internal state without exposing its internals. Used in:

  • Undo systems

  • Snapshots

  • Time-travel debugging (like Redux DevTools)

Example

class Editor {
  constructor() {
    this.content = "";
  }

  type(words) {
    this.content += " " + words;
  }

  save() {
    return { content: this.content };
  }

  restore(memento) {
    this.content = memento.content;
  }
}

const editor = new Editor();

editor.type("Hello");
const saved = editor.save();

editor.type("World!");
console.log(editor.content); // Hello World!

editor.restore(saved);
console.log(editor.content); // Hello

6. Chain of Responsibility Pattern

Passes requests through a chain until one handles it. Common in Express middleware and validation pipelines.

✔ When to Use

  • Middleware systems

  • Validation checks

  • Logging layers

Example

function step1(req, next) {
  req.count++;
  next();
}

function step2(req, next) {
  req.count++;
  next();
}

function runChain(req, ...fns) {
  let index = 0;

  function next() {
    if (index < fns.length) {
      const fn = fns[index++];
      fn(req, next);
    }
  }

  next();
}

const req = { count: 0 };
runChain(req, step1, step2);
console.log(req.count); // 2

Summary of Behavioral Patterns

Behavioral patterns help you:

  • Manage communication between components

  • Replace complex conditionals with flexible logic

  • Build modular, maintainable architectures

  • Implement undo/redo, strategies, iteration, middleware, and observers

These patterns are especially powerful in frontend frameworks and Node.js.


Real-World Use Cases

Design patterns become truly valuable when you apply them to real-world JavaScript applications. Modern frontend frameworks, state management tools, and backend systems rely heavily on these patterns—often without developers realizing it.

This section shows how design patterns power the tools you use every day, making it easier to understand why they matter and how to apply them in your own projects.

1. React: Observer + Composite Patterns

React’s core architecture is built on a combination of:

Observer Pattern

  • React components re-render when state changes

  • useState, useReducer, and context trigger updates to subscribed components

This is classic publish/subscribe behavior.

Composite Pattern

  • Components are composed of smaller components

  • Nesting creates component trees

  • Enables reusable UI structures

React Example: Composite

function Card({ children }) {
  return <div className="card">{children}</div>;
}

function App() {
  return (
    <Card>
      <h1>Hello</h1>
      <p>Reusable UI blocks!</p>
    </Card>
  );
}

2. Vue.js Reactivity: Proxy + Observer Patterns

Vue 3’s entire reactivity system is built on:

Proxy Pattern

  • new Proxy() intercepts get and set

  • Tracks dependencies automatically

Observer Pattern

  • Re-renders components when tracked state changes

Real Example

const state = reactive({ count: 0 });

// Proxy tracks changes automatically
watch(() => state.count, (newValue) => {
  console.log("Count changed:", newValue);
});

3. Node.js EventEmitter: Observer Pattern

Node’s built-in EventEmitter is a textbook example of the Observer Pattern.

const EventEmitter = require("events");
const emitter = new EventEmitter();

emitter.on("message", (msg) => console.log("Received:", msg));

emitter.emit("message", "Hello Node!");

Used in:

  • Streams

  • HTTP request lifecycle

  • WebSocket communication

4. Express Middleware: Chain of Responsibility + Decorator

Express routes process requests through middleware functions, a direct implementation of the Chain of Responsibility Pattern.

app.use((req, res, next) => {
  req.start = Date.now();
  next();
});

Each middleware layer modifies the request or response before passing it along.

Decorators also appear conceptually:

  • Authentication layers

  • Validation logic

  • Logging wrappers

5. Redux: Command + Observer + Singleton

Redux’s architecture combines multiple patterns:

Singleton Pattern

  • The store is a single global state container

  • Only one shared source of truth

Command Pattern

  • Actions describe operations

  • Reducers interpret these commands

Observer Pattern

  • Components subscribe to store updates

store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: "ADD_TODO", text: "Learn patterns" });

6. Axios / Fetch Wrapper: Facade Pattern

Developers often wrap HTTP clients with a Facade:

const api = {
  getUser(id) {
    return axios.get(`/users/${id}`);
  },
  createPost(data) {
    return axios.post("/posts", data);
  }
};

This abstracts:

  • Headers

  • Error handling

  • Retry logic

  • Base URL configuration

Result: a simple interface for complex networking logic.

7. Database Layer: Factory + Strategy Patterns

Apps often need different repositories depending on the environment:

  • Local SQLite for development

  • PostgreSQL for production

  • Mock DB for tests

A Factory decides which implementation to use.

class RepoFactory {
  static getRepo(env) {
    if (env === "prod") return new PostgresRepo();
    return new MemoryRepo();
  }
}

Strategy is used for:

  • Query strategies

  • Indexing strategies

  • Caching strategies

8. Microservices: Proxy + Facade Patterns

In microservice systems, API gateways act as:

Proxy

  • Routing

  • Rate limiting

  • Authentication

Facade

  • Aggregating multiple microservice responses into one

9. UI Libraries: Decorator Pattern (HOCs & Wrappers)

React HOC examples:

function withLogger(Component) {
  return function Wrapped(props) {
    console.log("Props:", props);
    return <Component {...props} />;
  };
}

Wrappers like this add:

  • Logging

  • Caching

  • Error boundaries

  • Performance tracking

10. CLI Tools & Build Systems: Builder Pattern

Tools like Webpack, Vite, Babel, and ESLint rely heavily on the Builder Pattern:

const config = builder
  .setEntry("index.js")
  .enableMinify()
  .addPlugin(new ReactPlugin())
  .build();

Summary of Real-World Use Cases

Design patterns are everywhere in JavaScript ecosystems:

Tool / Framework Patterns Used
React Observer, Composite
Vue Proxy, Observer
Node.js EventEmitter Observer
Express Chain of Responsibility, Decorator
Redux Singleton, Command, Observer
Axios/Fetch Wrappers Facade
Database Repos Factory, Strategy
Microservices Proxy, Facade
React HOCs Decorator
Build Tools Builder

Patterns are not theoretical—they solve real design challenges in almost every JS project.


Implementing Design Patterns in Modern JavaScript

Modern JavaScript (ES6+) provides powerful language features — classes, modules, arrow functions, Promises, async/await, Proxy, and more — making it easier than ever to implement design patterns cleanly and efficiently.

In this section, we’ll rewrite traditional design patterns using modern JS syntax, apply them to real use cases, and walk through an example refactor that transforms messy code into a scalable, pattern-based structure.

1. Using ES6 Classes for Object-Oriented Patterns

Many creational and structural patterns (Factory, Singleton, Builder, Adapter, Decorator) become more readable when implemented with ES6 classes.

Factory Pattern with ES6

class Car {
  constructor(model) {
    this.model = model;
  }
}

class CarFactory {
  static create(type) {
    const models = {
      sedan: "Toyota Camry",
      suv: "Honda CR-V",
      sport: "Mazda MX-5",
    };
    return new Car(models[type]);
  }
}

const car = CarFactory.create("sport");
console.log(car.model);

Benefits

  • Clearer structure

  • Easy extension

  • Familiar to developers from OOP backgrounds

2. Using JavaScript Modules for Singleton & Modular Architectures

JavaScript’s native ES Modules (import/export) automatically create singletons because the module is evaluated once and cached.

Modern Singleton

// config.js
export const config = {
  apiUrl: "https://api.example.com",
  version: "1.5",
};

// app.js
import { config } from "./config.js";

console.log(config.apiUrl);

This pattern is used everywhere:

  • Vuex/Pinia store instances

  • API clients

  • App configuration layers

3. Using Closures for Encapsulation (Module + Revealing Module)

Closures still provide unmatched encapsulation in JS.

Revealing Module Pattern (Modern Version)

const Counter = (() => {
  let count = 0;

  const increment = () => { count++; };
  const getCount = () => count;

  return {
    increment,
    getCount,
  };
})();

Counter.increment();
console.log(Counter.getCount());

Benefits:

  • Private variables

  • Secure state

  • Clean public interface

4. Using Proxy for Advanced Behavior (Validation, Caching, Reactivity)

The Proxy API makes implementing structural and behavioral patterns cleaner.

Validation Proxy

const user = {
  name: "",
  age: 0
};

const userProxy = new Proxy(user, {
  set(target, prop, value) {
    if (prop === "age" && value < 0) {
      throw new Error("Age cannot be negative");
    }
    target[prop] = value;
    return true;
  }
});

userProxy.age = 25;

Real Use

This is how Vue 3’s reactivity system works internally.

5. Using Generators for Iterator Pattern

JavaScript generators make custom iteration incredibly simple.

Iterator with Generator

function* numberGenerator(limit) {
  let i = 0;
  while (i < limit) {
    yield i++;
  }
}

for (const num of numberGenerator(3)) {
  console.log(num);
}

6. Using Higher-Order Functions for Strategy & Decorator Patterns

Functions are first-class citizens → ideal for behavioral patterns.

Strategy Pattern with Functions

const strategies = {
  uppercase: (str) => str.toUpperCase(),
  lowercase: (str) => str.toLowerCase(),
};

function transform(str, strategy) {
  return strategies[strategy](str);
}

console.log(transform("Hello", "uppercase"));

Decorator Pattern

const withLogging = (fn) => {
  return (...args) => {
    console.log("Args:", args);
    return fn(...args);
  };
};

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);

loggedAdd(2, 3);

7. Using Async/Await for Command & Chain of Responsibility Patterns

Async functions enable asynchronous processing flows.

Command Pattern organizing async actions

const command = {
  async execute() {
    await fetch("/api/data");
  }
};

Chain of Responsibility (async version)

async function runPipeline(input, ...steps) {
  for (const step of steps) {
    input = await step(input);
  }
  return input;
}

runPipeline(
  1,
  async (n) => n + 1,
  async (n) => n * 3
).then(console.log);

8. Example: Refactoring Messy Code With Patterns

Let’s take a spaghetti-like piece of code and transform it using design patterns.

Before: Messy, Hard-to-Maintain Code

async function checkout(cart, user) {
  if (!user || !user.id) throw new Error("Invalid user");
  if (cart.total < 1) throw new Error("Cart empty");

  const tax = cart.total * 0.1;

  const total = cart.total + tax;

  await fetch("/pay", {
    method: "POST",
    body: JSON.stringify({ total }),
  });

  await fetch("/notify", {
    method: "POST",
    body: JSON.stringify({ user }),
  });

  return "done";
}

Problems:

  • Validation mixed with logic

  • Hard-coded calculations

  • No extensibility

  • Difficult to test

After: Clean, Scalable, Pattern-Based Code

✔ Apply Strategy Pattern for tax calculation

✔ Apply Facade Pattern for payment + notifications

✔ Apply Chain of Responsibility for validation

Strategy Pattern (tax.js)

export const taxStrategy = {
  default: (total) => total * 0.1,
  premium: (total) => total * 0.08,
};

Validation Chain (validate.js)

export function validateUser(user) {
  if (!user || !user.id) throw new Error("Invalid user");
}

export function validateCart(cart) {
  if (cart.total < 1) throw new Error("Cart empty");
}

Facade (orderService.js)

export async function processOrder(total, user) {
  await fetch("/pay", { method: "POST", body: JSON.stringify({ total }) });
  await fetch("/notify", { method: "POST", body: JSON.stringify({ user }) });
}

Checkout (checkout.js)

import { validateUser, validateCart } from "./validate.js";
import { taxStrategy } from "./tax.js";
import { processOrder } from "./orderService.js";

export async function checkout(cart, user, strategy = "default") {
  validateUser(user);
  validateCart(cart);

  const tax = taxStrategy[strategy](cart.total);
  const total = cart.total + tax;

  await processOrder(total, user);
  return "done";
}

🚀 Benefits of Using Patterns

After refactoring:

  • Code is testable, with isolated modules

  • Logic is flexible and extendable

  • Validation, calculation, and process flows are decoupled

  • Clear architecture emerged from chaos

  • You can swap tax strategies or add new ones easily

  • You can enhance the Facade (e.g., logging, retry)

This demonstrates the power of design patterns in real-world JavaScript apps.


Design Patterns for Scalable Frontend Apps

Modern frontend applications—built with React, Vue, Angular, Svelte, or vanilla JavaScript—often grow quickly in size and complexity. Without a proper structure, they become difficult to maintain, test, or extend.

Design patterns play a crucial role in keeping frontend code modular, predictable, and scalable. This section focuses on how to apply patterns effectively in UI development.

1. Component Architecture (Composite Pattern)

Frontend frameworks rely heavily on the Composite Pattern, where components can contain other components. This structure allows you to build complex UIs from smaller, reusable elements.

Example

function Card({ children }) {
  return <div className="card">{children}</div>;
}

function App() {
  return (
    <Card>
      <h1>Hello</h1>
      <p>Reusable UI blocks</p>
    </Card>
  );
}

Benefits

  • Reusable UI blocks

  • Nestable structure

  • Clear separation of responsibility

This pattern is fundamental to React, Vue, and Angular component systems.

2. Event Management (Observer Pattern)

UIs are event-driven by nature: clicks, inputs, hovers, scrolls, network responses, and more.

The Observer Pattern powers:

  • React state updates

  • Vue’s reactivity

  • Browser DOM events

  • Custom event buses

  • Pub/Sub for inter-component communication

Example: Simple Event Bus

const EventBus = {
  events: {},

  on(event, listener) {
    (this.events[event] = this.events[event] || []).push(listener);
  },

  emit(event, data) {
    (this.events[event] || []).forEach((callback) => callback(data));
  },
};

Great for:

  • Cross-component communication in Vue

  • Toast/notification systems

  • Global event listeners

3. State Management (Singleton + Observer + Command)

Scalable apps require predictable and centralized state management.

State management libraries use patterns:

Singleton Pattern

  • Only one store exists (Redux, Pinia, Zustand)

Observer Pattern

  • Components subscribe to state changes

Command Pattern

  • Actions represent discrete operations

Example (Redux-style)

store.subscribe(() => {
  console.log("State changed:", store.getState());
});

4. Form Handling (Strategy Pattern)

Forms often require different validation strategies:

  • Email must match regex

  • Username must be unique

  • Password must follow rules

Using the Strategy Pattern improves maintainability:

const validators = {
  email: (value) => /\S+@\S+\.\S+/.test(value),
  username: (value) => value.length > 3,
  password: (value) => value.length >= 8,
};

function validate(type, value) {
  return validators[type](value);
}

This pattern keeps validation clean and modular.

5. UI State Transitions (State Pattern)

The State Pattern helps manage UI flows where a component behaves differently depending on its current state.

Examples:

  • Loading → Success → Error

  • Steps in a wizard

  • Authentication flow

Example

const states = {
  loading: () => "Loading...",
  success: (data) => `Loaded: ${data}`,
  error: () => "Error",
};

function render(state, data) {
  return states[state](data);
}

console.log(render("success", "Hello World"));

6. Styling Patterns (Decorator + Strategy)

CSS-in-JS, utility-first CSS, and component-based styling are perfect use cases for Decorator and Strategy patterns.

Decorator Pattern (Theme wrapper)

function withTheme(Component) {
  return function WrappedComponent(props) {
    return <Component {...props} theme="dark" />;
  };
}

Strategy Pattern (CSS variants)

const buttonVariants = {
  primary: "bg-blue-500 text-white",
  secondary: "bg-gray-500 text-white",
};

7. Optimizing API Calls (Facade Pattern)

Frontend apps often need a clean wrapper around messy backend APIs.

Facade Example

const Api = {
  getUser(id) {
    return fetch(`/api/users/${id}`).then((res) => res.json());
  },
  updateUser(id, data) {
    return fetch(`/api/users/${id}`, {
      method: "PUT",
      body: JSON.stringify(data),
    });
  },
};

This creates a clean API layer for the UI.

8. Performance Optimization (Proxy + Lazy Loading)

A Proxy can help with:

  • Lazy-loading images

  • Caching results

  • Intercepting expensive operations

Lazy Loading Proxy

9. Shared UI Logic (Decorator + Higher-Order Components)

React and Vue often share logic using decorators or wrappers:

React HOC Example

function withLogger(Component) {
  return function Wrapped(props) {
    console.log("Rendering with props:", props);
    return <Component {...props} />;
  };
}

This pattern helps isolate cross-cutting concerns.

10. Routing (Strategy + Observer)

Frontend routing systems use:

  • Strategy Pattern → handling different navigation behaviors

  • Observer Pattern → listen to route changes

Frameworks implementing these:

  • React Router

  • Vue Router

  • Angular Router

11. Component Reusability (Template Method Pattern)

Components that share behavior but differ in implementation can use the Template Method Pattern.

Example:

class Dialog {
  open() {
    this.beforeOpen();
    console.log("Dialog opened");
    this.afterOpen();
  }

  beforeOpen() {}
  afterOpen() {}
}

class AlertDialog extends Dialog {
  beforeOpen() {
    console.log("Preparing alert...");
  }
}

Used in:

  • Reusable modals

  • Base components

  • UI scaffolding

12. UI Data Transformations (Strategy Pattern)

Different UIs may need different formatting strategies:

  • Currency

  • Date formats

  • Sorting

  • Filtering

Example

const formatters = {
  currency: (v) => `$${v.toFixed(2)}`,
  date: (v) => new Date(v).toLocaleDateString(),
};

🚀 Summary of Frontend Design Pattern Applications

Need Pattern
Component composition Composite
Events & reactivity Observer
Global state Singleton / Command
Validation Strategy
UI flows State
Shared logic Decorator / HOC
API abstractions Facade
Performance Proxy
Routing Strategy / Observer

These patterns help you build clean, modular, and scalable frontend applications—regardless of framework.


Design Patterns for Scalable Backend (Node.js)

Node.js powers millions of backend services, APIs, and microservices. Its event-driven, non-blocking architecture makes it an excellent fit for design patterns—many of which are used implicitly in popular frameworks like Express, NestJS, Fastify, and Koa.

This section covers how design patterns help you build scalable, maintainable, and high-performance backends in Node.js.

1. Layered Architecture (Facade + Strategy + Singleton)

A scalable Node.js backend typically follows a layered structure:

  1. Controller Layer – handles HTTP requests

  2. Service Layer – business logic

  3. Repository Layer – data management (DB, cache, external APIs)

  4. Utility Layer – shared helpers

Each layer uses design patterns:

✔ Facade Pattern

Controllers act as facades to abstract complex actions.

✔ Strategy Pattern

Multiple services or repositories can be swapped depending on the environment.

✔ Singleton Pattern

Database clients (MongoDB, Prisma, Sequelize, Redis) are singletons.

2. Repository Pattern (Factory + Strategy)

Repositories abstract database operations.

✔ Why use it?

  • You can switch DBs (MongoDB → PostgreSQL) without changing services

  • Logic becomes testable

  • API is consistent

Example

class UserRepo {
  getById(id) {}
  create(data) {}
}

class MongoUserRepo extends UserRepo {
  getById(id) {
    return mongo.collection("users").findOne({ id });
  }
}

class PostgresUserRepo extends UserRepo {
  getById(id) {
    return pg.query("SELECT * FROM users WHERE id=$1", [id]);
  }
}

class RepoFactory {
  static getUserRepo(env) {
    return env === "prod" ? new PostgresUserRepo() : new MongoUserRepo();
  }
}

This uses:

  • Factory (selecting repo)

  • Strategy (multiple repo implementations)

3. Middleware Pipeline (Chain of Responsibility + Decorator)

Node.js frameworks use middleware pipelines:

  • Express

  • Koa

  • Fastify

  • NestJS (filters & guards)

Each middleware passes the request to the next, matching the Chain of Responsibility Pattern.

Example (Express-style)

app.use((req, res, next) => {
  req.startTime = Date.now();
  next();
});

Decorators appear conceptually when you add features like:

  • Logging

  • Auth

  • Validation

  • Rate limiting

4. Caching Layer (Proxy Pattern)

A Proxy can wrap expensive operations (like DB queries) to add caching.

Example: Caching Proxy

function withCache(fn) {
  const cache = new Map();

  return async (key) => {
    if (cache.has(key)) return cache.get(key);

    const result = await fn(key);
    cache.set(key, result);
    return result;
  };
}

const getUserCached = withCache((id) => db.users.findById(id));

Used in:

  • Redis caching

  • API rate-limiters

  • Query performance boosting

5. Event-Driven Architecture (Observer Pattern)

Node’s built-in EventEmitter and message queues rely on the Observer Pattern.

Used for:

  • Background jobs

  • Logs

  • Notifications

  • WebSocket events

  • Microservice communication

Example

const EventEmitter = require("events");
const bus = new EventEmitter();

bus.on("user_registered", (u) => console.log("Welcome:", u));
bus.emit("user_registered", { id: 1 });

6. Validation Layer (Strategy Pattern)

Backend validation often varies by:

  • Feature

  • API version

  • Data type

Example

const validators = {
  user: (data) => !!data.email,
  product: (data) => !!data.price,
};

function validate(type, data) {
  return validators[type](data);
}

This makes the system extendable and clean.

7. Authentication & Authorization (Strategy + Chain of Responsibility)

Modern applications support:

  • JWT

  • OAuth

  • API keys

  • Passport.js strategies

Strategy Pattern

Multiple authentication “strategies” plug into the service.

Chain of Responsibility

Auth → Role Check → Controller

app.get("/admin",
  authMiddleware,
  roleMiddleware("admin"),
  controller
);

8. API Clients (Adapter + Facade)

Node backends often integrate with external services:

  • Stripe

  • AWS

  • Firebase

  • Payment gateways

  • Email providers

Using patterns ensures consistency.

Adapter Pattern

class StripeAdapter {
  constructor(stripe) {
    this.stripe = stripe;
  }

  charge(amount) {
    return this.stripe.paymentIntents.create({ amount });
  }
}

Facade Pattern

const PaymentService = {
  pay(amount) {
    return stripeAdapter.charge(amount);
  }
};

9. Logger System (Singleton + Decorator)

Logging libraries like Winston and Pino use a Singleton for the logger instance.

Decorator to wrap functions

function withLogging(fn) {
  return (...args) => {
    console.log("Calling:", fn.name);
    return fn(...args);
  };
}

Used for:

  • Request logging

  • Error reporting

  • Performance monitoring

10. Config Management (Singleton Pattern)

Your config file is a classic Singleton:

export default {
  port: 3000,
  db: "localhost",
};

Shared by:

  • Database

  • Services

  • Auth system

  • API layer

11. Job Queues (Command + Observer)

Backends often use:

  • BullMQ

  • RabbitMQ

  • Kafka

  • AWS SQS

Patterns involved:

  • Command = job

  • Observer = worker listening for jobs

  • Strategy = job handlers per type

12. Microservices Architecture (Proxy + Facade + Adapter)

Microservices use patterns everywhere:

API Gateway = Proxy

  • Route requests

  • Apply rate limits

  • Handle caching

Aggregators = Facade

  • Combine multiple microservices

  • Produce simplified responses

Service Clients = Adapter

  • Normalize differences between services

🚀 Summary of Backend Design Pattern Applications

Need Pattern
Layered architecture Facade, Strategy, Singleton
ORM/DB access Repository, Factory, Strategy
Middleware Chain of Responsibility
Caching Proxy
Events Observer
Validation Strategy
Authentication Strategy + Chain of Responsibility
External integrations Adapter + Facade
Logs Decorator + Singleton
Config Singleton
Queues Command + Observer
Microservices Proxy, Facade, Adapter

Design patterns make backend code clean, modular, testable, and scalable — especially in Node.js environments.


Best Practices

Design patterns are powerful, but like any tool, they must be used wisely. Applying patterns correctly leads to cleaner, more scalable, and more maintainable applications. Applying them incorrectly can result in unnecessary complexity and “architectural overkill.”

This section outlines the best practices you should follow when using design patterns in JavaScript.

1. Keep It Simple First (KISS Principle)

Always start with the simplest possible solution.

Design patterns should not be used automatically—only when:

  • Code becomes repetitive

  • Logic becomes complex

  • Multiple modules need to interact

  • You notice clear pain points

Avoid the trap of forcing patterns into code that doesn’t need them.

2. Identify Real Problems Before Choosing a Pattern

Patterns solve specific categories of problems:

  • Too many conditionals → Strategy Pattern

  • Messy API calls → Facade Pattern

  • Duplicate object creation → Factory Pattern

  • Hard-to-track events → Observer Pattern

  • Need for extensible logic layers → Decorator Pattern

Don’t choose a pattern first—identify the problem, then pick the pattern that solves it.

3. Keep Each Pattern in Its Appropriate Layer

For good architecture:

  • UI layer: Composite, Observer, State

  • Service layer: Strategy, Template, Facade

  • Data layer: Repository, Factory

  • Infrastructure: Proxy, Adapter, Singleton

Patterns fit naturally into certain layers—using them in the wrong place leads to confusion.

4. Favor Composition Over Inheritance

JavaScript supports inheritance, but composition is usually easier to maintain.

Prefer:

const service = {
  ...loggerMixin,
  ...cacheMixin,
};

Instead of:

class MyService extends Logger {
  // deep inheritance tree
}

Composition enables:

  • More flexibility

  • Less coupling

  • Easier testing

5. Avoid Overengineering

Using patterns without need leads to “Architecture Astronaut Syndrome.”

Signs of overengineering:

  • Layers that don’t solve real problems

  • Classes with no purpose

  • Wrappers around simple functions

  • Too many abstractions for small apps

If your app is small, don’t add patterns “just because.”

6. Document Your Patterns Clearly

Design patterns introduce architecture and abstraction—so documentation becomes essential.

Document:

  • Why the pattern was chosen

  • How it works in your project

  • How other developers should use it

This avoids confusion when onboarding team members.

7. Test Each Pattern’s Component in Isolation

Patterns shine when components are:

  • Decoupled

  • Independent

  • Modular

This makes them easier to unit test.

Examples:

  • Test each strategy independently

  • Test a repo without touching the DB

  • Mock observers or services

  • Test decorators by wrapping simple functions

8. Ensure Patterns Enhance, Not Obscure, Readability

Patterns should improve clarity, not hide logic.

Good:

const tax = taxStrategy[type](amount);

Bad:

// unnecessary layers
strategy.compute.execute(type).calculate(amount);

If someone has to study the architecture for 20 minutes before understanding a function, the pattern is hurting the project.

9. Use Pattern Combinations Judiciously

Some patterns naturally complement each other:

  • Observer + Proxy (Vue Reactivity)

  • Facade + Adapter (API Clients)

  • Strategy + Factory (Service Selection)

  • Singleton + Command (Redux Store + Actions)

  • Decorator + Chain of Responsibility (Express Middleware)

But don’t combine patterns just to make the architecture “fancy.”
Use them only when they provide clear benefits.

10. Refactor Toward Patterns Gradually

Never try to apply all patterns up front.

Instead:

  1. Start with plain code

  2. Identify repeated logic or complexity

  3. Extract parts into appropriate patterns

  4. Document and test

  5. Continue improving iteratively

Gradual refactoring ensures:

  • Lower risk

  • Cleaner code

  • Stable deployments

11. Use JavaScript Features to Simplify Patterns

Modern JS reduces the boilerplate needed for many patterns:

  • Proxy for Observer, Validation, and Reactivity

  • Modules for Singleton and Encapsulation

  • Classes for Factory or Builder

  • Functions as values for Strategy and Decorator

  • Generators for Iterator

  • Async/Await for Command chains

Patterns become much easier with ES6+.

12. Reuse Established Patterns Instead of Reinventing Solutions

Many patterns already exist in:

  • React hooks

  • Vue composables

  • Express middleware

  • NestJS providers

  • Redux reducers

  • Axios interceptors

Use these built-in implementations instead of reinventing the wheel.

13. Maintain Clean Naming Conventions

Names should clearly communicate the pattern:

  • UserRepository

  • OrderService

  • CacheProxy

  • PaymentStrategy

  • ApiFacade

  • LoggerDecorator

Good naming helps teammates immediately understand the architecture.

14. Keep Dependencies Weak (Low Coupling, High Cohesion)

Patterns are meant to decouple your code.

Avoid strong ties between modules.
Use interfaces or abstract methods (conceptually, even without TypeScript) to keep components independent.

15. Follow SOLID Principles Where Appropriate

Many JS patterns map directly to SOLID principles:

  • S (Single Responsibility) → Strategy, Adapter

  • O (Open/Closed) → Decorator

  • L (Liskov) → Subclasses in Template Method

  • I (Interface Segregation) → Repos

  • D (Dependency Inversion) → Factories, DI containers

SOLID + design patterns = stable, scalable architecture.

16. Choose Patterns Based on Application Scale

Small app:

  • Module

  • Singleton

  • Strategy

Medium app:

  • Facade

  • Repository

  • Observer

Large app:

  • Composite

  • Proxy

  • Chain of Responsibility

  • Template Method

  • DI + Factories

Select patterns based on real needs, not trends.

Summary of Best Practices

Design patterns should make your code:

✔ Clear
✔ Modular
✔ Testable
✔ Predictable
✔ Scalable

Avoid:

✘ Overengineering
✘ Excessive abstraction
✘ Using patterns without a problem
✘ Creating unnecessary layers

The best architects use patterns thoughtfully, not aggressively.


Conclusion

Design patterns are not just theoretical concepts reserved for large enterprise systems—they are practical, everyday tools that help JavaScript developers build clean, scalable, and maintainable applications. Whether you're working on a small frontend project or a large distributed backend system, patterns give your architecture structure, flexibility, and long-term stability.

Throughout this tutorial, you’ve learned how to design patterns:

  • Solve common architectural challenges

  • Make codebases easier to understand and extend

  • Improve separation of concerns

  • Reduce complexity and duplication

  • Enhance testability and scalability

  • Provide a shared vocabulary for developers

You also explored how real-world frameworks and libraries—React, Vue, Node.js, Express, Redux, and more—use these patterns under the hood, proving their value in everyday JavaScript development.

🎯 When to Use Design Patterns

Use patterns when they help:

  • Simplify complex logic

  • Centralize duplicated behavior

  • Improve component communication

  • Abstract or encapsulate messy implementations

  • Introduce flexibility for future changes

Avoid patterns when:

  • The code is simple

  • They add unnecessary layers

  • They obscure readability

Patterns should always serve clarity—not complexity.

🧰 What You Can Build Using Patterns

With design patterns, you can confidently build:

  • Large-scale frontend apps with reusable components

  • Robust backend APIs with clean layers

  • Testable and modular architectures

  • Reactive UIs and declarative logic

  • Extensible microservice systems

  • Maintainable legacy refactors

Patterns give you a toolbox for solving recurring problems in consistent, proven ways.

🚀 Final Advice

Keep these practices in mind:

  • Start simple

  • Add patterns only when needed

  • Compose instead of inherit

  • Document your architecture

  • Refactor incrementally

  • Use modern JavaScript features to simplify implementations

Design patterns are most powerful when used thoughtfully and sparingly.

You can find the full source code on our GitHub.

That's just the basics. If you need more deep learning about JavaScript, you can take the following cheap course:

Thanks!