Mar 6, 2025

Understanding Design Patterns in JavaScript: A Beginner's Guide

Tags

When many developers start in JavaScript, we usually just “make it work”: add a few functions, throw some objects around, maybe sprinkle in a library or two. That works for small scripts, but as soon as a project grows, say a dashboard with authentication, API calls, and real‑time updates, the codebase can start to feel like a ball of wires. That is exactly when design patterns start paying off because they give us reusable ways to structure code so that adding a new feature does not break three others.


Over time, a few patterns naturally keep showing up in typical JS work: one object that should exist only once, a helper that creates different objects depending on data, a module that hides internal details, and a system where many parts react to one event. Those are the Singleton, Factory, Module, and Observer patterns, and they map very well to real JavaScript projects.


design-patterns-js


Creational patterns: how we create things


Creational patterns focus on how objects are created so that the rest of the code does not need to care about construction details. When a project grows, putting creation logic in one place helps keep everything consistent and testable.


Singleton pattern: one central “brain”


On one of our internal tools, multiple parts of the app needed access to configuration: API base URLs, feature flags, and the current logged‑in user. Initially, there were separate config objects imported in different files, and they slowly drifted out of sync. The bug was subtle: one module talked to an old endpoint while the rest of the app used a new one.


A Singleton cleaned this up by making sure there is exactly one configuration “brain” in the entire application, no matter how many times it is imported. We used a simple closure‑based approach so that any module calling getConfig() always received the same shared object.



function createConfig() {
  let config = null;

  return function getConfig() {
    if (!config) {
      config = {
        apiBaseUrl: "https://api.my-apps.dev",
        featureFlags: { newDashboardArea: true },
      };
    }
    return config;
  };
}

const getConfig = createConfig();

const a = getConfig();
const b = getConfig();

console.log(a === b); // true


The important idea here is not the syntax but the intent: there is one source of truth, and everyone reads from it. We still remain careful with Singletons because they can turn into “hidden globals,” but for things that genuinely must be shared (like configuration or a logging service), they keep the code predictable.​


Factory pattern: letting one place decide what to build


A different problem surfaced when building a UI that displayed different cards depending on user type: “guest”, “member”, or “admin”. Each card had slightly different behavior and styling. Early on, the code was full of if (type === "admin") checks scattered across the app, which made adding a fourth type painful.


A Factory function centralized that decision:



function createUserCard(user) {
  if (user.role === "admin") {
    return {
      render() {
        console.log(`Admin panel for ${user.name}`);
      },
    };
  }

  if (user.role === "member") {
    return {
      render() {
        console.log(`Member dashboard for ${user.name}`);
      },
    };
  }

  return {
    render() {
      console.log(`Welcome page for guest ${user.name}`);
    },
  };
}

const alice = createUserCard({ name: "Alice", role: "admin" });
const bob = createUserCard({ name: "Bob", role: "guest" });

alice.render();
bob.render();


Later, when “support” users were introduced, the only file that needed editing was this factory. Everywhere else continued to call createUserCard, which is exactly the point: one spot knows the construction details; the rest of the app just says “give me the right thing for this data”.​



Structural patterns: organizing and hiding complexity


Structural patterns are about how pieces fit together and how much of that structure the outside world is allowed to see. JavaScript’s functions and closures make it very natural to hide some details while exposing a clear interface.​


Module pattern: keeping private things private


In one of our older frontend projects (pre‑ES modules), the global namespace was crowded: several files defined utilsconfig, and other generic names. Loading them in the wrong order caused strange bugs. The Module pattern helped by letting us wrap logic inside a function and return only what callers truly needed.



const authModule = (function () {
  let token = null;

  function setToken(newToken) {
    token = newToken;
  }

  function isLoggedIn() {
    return Boolean(token);
  }

  function getAuthHeader() {
    if (!token) return {};
    return { Authorization: `Bearer_ ${token}` };
  }

  return {
    setToken,
    isLoggedIn,
    getAuthHeader,
  };
})();

authModule.setToken("secret-123");
console.log(authModule.isLoggedIn()); // true
console.log(authModule.getAuthHeader()); // { Authorization: 'Bearer secret-123' }


Internally, token never leaks out, which means no other part of the app can accidentally overwrite it. In modern projects, ES modules offer a built‑in way to do this: you keep helpers in the file and only export what should be public, but the mindset is the same—expose a simple surface, hide the wiring.​


Behavioral patterns: how pieces talk to each other


Behavioral patterns deal with communication between objects and functions. In JavaScript, this shows up heavily in UI updates, real‑time features, and any app that reacts to events.​


Observer pattern: many listeners, one event


On a dashboard project that pulled live metrics from a WebSocket, multiple parts of the UI needed to react whenever new data arrived: a chart, a notification banner, and a small counter in the header.
Initially, the WebSocket callback directly manipulated all three, which made it hard to change one widget without touching the others.


Refactoring to an Observer style made each widget subscribe to updates instead. The data source became the “subject,” and widgets became “observers”.



function createSubject() {
  const observers = [];

  function subscribe(fn) {
    observers.push(fn);
    return () => {
      const idx = observers.indexOf(fn);
      if (_idx !== -1) observers.splice(_idx, 1);
    };
  }

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

  return { subscribe, notify };
}

const metricsStream = createSubject();

const unsubscribeLog = metricsStream.subscribe(data => {
  console.log("Log widget:", data);
});

const unsubscribeCounter = metricsStream.subscribe(data => {
  console.log("Counter widget: total =", data.total);
});

metricsStream.notify({ total: 5, status: "ok" });
metricsStream.notify({ total: 8, status: "warning" });

unsubscribeLog();
metricsStream.notify({ total: 10, status: "ok" });


Now the metrics source does one thing: emit updates. Widgets decide individually how to react or when to unsubscribe. This same pattern appears in many JS libraries: event emitters, Redux‑like stores, and even basic DOM events.


Putting patterns together in real projects


On a realistic JavaScript app, for example, a React or vanilla JS dashboard, you rarely use just one pattern in isolation. A typical stack might:​

  • Use a Singleton‑style configuration so API endpoints and feature flags come from a single place.
  • Rely on Factories to construct UI blocks or services depending on the environment (development vs production).
  • Wrap features inside Modules so only the public API is exported from each file.
  • Model UI updates and notifications using Observer‑style subscription mechanisms.

The goal is not to memorise names but to recognise pain points:

  • “We keep duplicating creation logic” → Factory or Builder.
  • “Everyone is fighting over shared configuration” → Singleton or centralised store.
  • “Global variables are leaking everywhere” → Module or proper ES module exports.
  • “Too many parts depend on one event” → Observer or a pub‑sub flavour.


Once those pains are visible, picking a pattern feels natural instead of academic.


EmoticonEmoticon