Mid-level 9 min · March 06, 2026

JS Design Patterns — Singleton State Leaks in Tests

Tests pass alone but fail together? Singleton mutations persist across Node.js test files.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Design patterns are reusable blueprints for solving recurring architectural problems
  • Creational patterns (Factory, Singleton) control how objects are created
  • Behavioral patterns (Observer, Strategy) manage communication between objects
  • Modern JS (ES2022+ private fields, modules) makes implementations cleaner
  • Biggest mistake: over-engineering with a pattern when a simpler solution exists
  • Production insight: wrong pattern choice adds coupling and makes debugging harder
✦ Definition~90s read
What is Design Patterns in JavaScript?

Design patterns in JavaScript are reusable solutions to common software design problems, formalized by the Gang of Four (GoF) book but adapted for JavaScript's prototypal inheritance and dynamic typing. They exist because raw code organization without structure leads to tight coupling, duplicated logic, and unmaintainable spaghetti — especially in large front-end apps or Node.js services.

Imagine you're building with LEGO.

Patterns like Singleton, Factory, Observer, Module, and Strategy each solve a specific category of problem: controlling object creation, managing state changes, encapsulating logic, or swapping behavior at runtime. You'll see these patterns in frameworks like React (Observer via hooks), Redux (Singleton stores), and Express (Strategy for middleware).

Don't use patterns as dogma — over-engineering with them when a simple function suffices is a common junior mistake. The real value comes from recognizing when a pattern naturally emerges from your problem, not forcing one in. In testing, patterns like Singleton become dangerous because their global state persists across test cases, causing cascading failures that are notoriously hard to debug — a key pain point this article addresses.

Plain-English First

Imagine you're building with LEGO. Instead of figuring out from scratch how to build a car every time, you follow a proven blueprint. Design patterns are those blueprints — battle-tested solutions to problems that developers have already solved a thousand times before. You don't copy the blueprint exactly, you adapt it. The point isn't to memorise shapes — it's to recognise the problem and know which blueprint fits.

Every production JavaScript codebase eventually grows to a point where 'just making it work' stops being enough. Components start talking to each other in ways nobody planned. State changes ripple unpredictably through the app. A bug fix in one place breaks three things elsewhere. That's not bad luck — it's what happens when code lacks structural intent. Design patterns are the antidote. They're a shared vocabulary between developers and a set of proven architectural decisions that prevent entire categories of bugs before they're written.

The problem design patterns solve is coupling — the invisible glue that binds unrelated parts of your code together. When your PaymentService directly instantiates a Logger, and your Logger directly reads from Config, and your Config mutates global state — you've built a house of cards. Change one thing, rebuild everything. Patterns like Singleton, Observer, and Factory give you controlled, intentional relationships between components instead of accidental ones.

By the end of this article you'll be able to recognise which pattern fits a given problem, implement each one correctly in modern JavaScript (including ES2022+ class features), understand the performance and memory implications of each, and spot the subtle bugs that come from applying them carelessly. This isn't a glossary tour — it's a practical field guide you'll actually use.

Why Singleton State Leaks in Tests

A design pattern is a reusable solution to a recurring problem in software design. In JavaScript, design patterns are implemented using the language's prototypal inheritance, closures, and module systems. The Singleton pattern ensures a class has only one instance and provides a global point of access to it. This is achieved by caching the instance in a static property or module scope, returning the same object on every instantiation attempt. The core mechanic is simple: a private constructor or factory function that checks for an existing instance before creating a new one. In practice, Singletons in JavaScript often rely on module caching (e.g., ES6 modules are singletons by default) or explicit instance management. Key properties: global state, lazy initialization, and a single point of control. This makes them useful for managing shared resources like configuration objects, database connections, or logging services. However, the same global state that makes Singletons convenient also makes them dangerous in test environments. When tests run in parallel or sequentially, a Singleton's cached state can persist across test cases, causing false positives or failures. Use Singletons sparingly, only for truly application-wide resources where state mutation is minimal or nonexistent. In real systems, they are often replaced with dependency injection or context objects to improve testability and isolate side effects.

Singleton ≠ Stateless
A Singleton that holds mutable state is a global variable in disguise. Tests that mutate that state will leak into other tests, breaking isolation.
Production Insight
A team used a Singleton for user session data in a Node.js API. When running integration tests in parallel, test A logged in as user X, test B logged in as user Y — the Singleton's cached session object was overwritten, causing both tests to fail with authentication errors.
The exact symptom: tests pass in isolation but fail when run together, with seemingly random '401 Unauthorized' or '403 Forbidden' errors.
Rule of thumb: if a Singleton holds mutable state, inject it as a dependency so each test can provide a fresh instance or mock.
Key Takeaway
Singletons are global state — treat them with the same caution as global variables.
In tests, mutable Singleton state causes non-deterministic failures that are hard to debug.
Prefer dependency injection or factory functions over Singletons for testable code.
Singleton State Leaks in Tests THECODEFORGE.IO Singleton State Leaks in Tests How shared state causes test pollution and how to avoid it Singleton Instance Global state shared across tests Test A Mutates State Changes internal data of singleton Test B Reads State Expects clean state but gets mutated Test Pollution Flaky tests due to shared state Reset Singleton Reinitialize state before each test ⚠ Singleton state persists across tests Always reset or recreate singleton in test setup THECODEFORGE.IO
thecodeforge.io
Singleton State Leaks in Tests
Design Patterns Javascript

The Creational Powerhouse: The Factory Pattern

The Factory pattern provides an interface for creating objects, but allows subclasses or a central logic unit to alter the type of objects that will be created. In JavaScript, this is exceptionally useful when your application needs to generate different types of components (like different UI buttons or database connectors) based on environment variables or user input without hardcoding new ClassName() throughout your logic.

Simple enough. But the real win isn't just centralised creation — it's that your callers never need to know the concrete class. They work against an interface or a common contract. Swap the implementation later without touching callers.

Where people trip up: using a Factory for a single implementation. That's just indirection without benefit. And factories that return different shapes under the same name cause runtime errors that type checkers won't catch.

io/thecodeforge/patterns/ForgeFactory.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * io.thecodeforge - Factory Pattern Implementation
 */
class DatabaseConnector {
    constructor(config) {
        this.config = config;
    }
    connect() { throw new Error("Method 'connect()' must be implemented."); }
}

class PostgreSqlConnector extends DatabaseConnector {
    connect() { return `Connected to Postgres at ${this.config.host}`; }
}

class MongoDbConnector extends DatabaseConnector {
    connect() { return `Connected to MongoDB at ${this.config.uri}`; }
}

class DBFactory {
    static createConnection(type, options) {
        switch(type) {
            case 'SQL': return new PostgreSqlConnector(options);
            case 'NoSQL': return new MongoDbConnector(options);
            default: throw new Error("Unsupported DB type");
        }
    }
}

const db = DBFactory.createConnection('SQL', { host: 'localhost' });
console.log(db.connect());
Output
Connected to Postgres at localhost
Factory as Restaurant Kitchen
  • Client orders by type (e.g., 'SQL'), not by class name
  • Kitchen (factory) decides which chef (class) to dispatch
  • Menu (interface) guarantees a product with expected methods
  • Adding a new dish (class) doesn't change the ordering process
  • Overusing the kitchen for a single dish adds needless complexity
Production Insight
Factory pattern in Node.js: each require caches the module. If your factory returns a new instance each time, you avoid singleton side effects. If you accidentally return a cached instance, you share state.
Common production failure: a factory that returns a promise but is treated synchronously — you get a Promise object instead of the actual connector.
Rule: always test factory output with .toBeInstanceOf or a duck-type check. Never trust the returned shape.
Key Takeaway
Factory decouples creation from usage.
Add new implementations without changing callers.
Don't abstract a single concrete class — that's just ceremony.
Should You Use a Factory?
IfOnly one implementation of the product exists
UseSkip the factory. Use new directly or a simple function.
IfYou need to switch implementations at runtime
UseFactory is the right choice. It centralises the switching logic.
IfProduct creation requires complex configuration or async operations
UseFactory method is fine. Consider Abstract Factory if you have families of related products.

The Singleton Pattern: Ensuring a Single Source of Truth

A Singleton restricts a class to a single instance and provides a global point of access to it. While often criticized if overused as 'global state,' it is vital for resource-heavy objects like Database Pools or Global Configuration managers where multiple instances would cause memory leaks or race conditions.

JavaScript's module system makes singletons deceptively easy — a module-level variable survives as long as the process. But that's also the trap: in a serverless environment, each cold start gets a new instance, but warm starts reuse the same. That inconsistency breaks assumptions.

Modern ES2022+ private static fields (#instance) give you real encapsulation. The old pattern of storing the instance on the constructor function itself is still common but less clean.

io/thecodeforge/patterns/ForgeSingleton.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * io.thecodeforge - Modern ES6 Singleton using Static Private Fields
 */
class GlobalConfig {
    static #instance = null;

    constructor() {
        if (GlobalConfig.#instance) {
            return GlobalConfig.#instance;
        }
        this.settings = { theme: 'dark', version: '2.1.0' };
        GlobalConfig.#instance = this;
    }

    static getInstance() {
        if (!this.#instance) {
            this.#instance = new GlobalConfig();
        }
        return this.#instance;
    }
}

const configA = new GlobalConfig();
const configB = GlobalConfig.getInstance();
console.log(configA === configB);
Output
true
Singleton Testing Trap
Singletons make unit testing a nightmare because they persist state between tests. Always provide a static reset method. Better yet, use a factory that returns the singleton lazily, and inject it where needed.
Production Insight
In Node.js, singletons are process-scoped. If your app runs in a cluster or serverless environment, each worker has its own singleton. That means shared state is not truly shared across workers — you'll need Redis or similar.
Common production failure: a singleton that holds a database connection pool. If the pool is exhausted, the entire process becomes unresponsive because every request shares the same limited resource.
Rule: for singletons that manage external resources, add a health check and auto-recovery mechanism. Never let a broken singleton kill your whole process.
Key Takeaway
Singleton = one instance per process.
Provide reset for tests.
Don't use it for mutable global state across requests.
When to Use a Singleton
IfYou need exactly one instance of a resource (DB pool, config, logger)
UseUse a singleton with a reset method for testing.
IfThe object doesn't hold state or is stateless
UseDon't use a singleton. A module with exported functions works fine.
IfYou're in a serverless environment with concurrent invocations
UseAvoid singletons with mutable state. Use external storage for shared state.

The Behavioral King: The Observer Pattern

The Observer pattern (the core of 'Pub/Sub' logic) allows an object (the Subject) to notify a list of 'Observers' about state changes. This is how event listeners work in the browser and how frameworks like React and Vue trigger UI updates when state changes.

The simplicity of Observer is also its biggest risk: observers hold references to subscribers, and if those subscribers aren't properly cleaned up, you get memory leaks and ghost callbacks. In a single-page application, forgetting to unsubscribe on component unmount is the number one cause of stale state and unexpected behaviour.

Modern JavaScript provides WeakRef and FinalizationRegistry to help, but most common implementations just store arrays of callbacks — manual cleanup is your responsibility.

io/thecodeforge/patterns/ForgeObserver.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * io.thecodeforge - Simple Event Bus (Observer Pattern)
 */
class EventBus {
    constructor() {
        this.subscribers = {};
    }

    subscribe(event, callback) {
        if (!this.subscribers[event]) this.subscribers[event] = [];
        this.subscribers[event].push(callback);
    }

    publish(event, data) {
        if (!this.subscribers[event]) return;
        this.subscribers[event].forEach(cb => cb(data));
    }

    unsubscribe(event, callback) {
        if (!this.subscribers[event]) return;
        this.subscribers[event] = this.subscribers[event].filter(cb => cb !== callback);
    }
}

const forgeBus = new EventBus();
forgeBus.subscribe('USER_LOGIN', (user) => console.log(`Welcome, ${user}!`));
forgeBus.publish('USER_LOGIN', 'ForgeAdmin');
Output
Welcome, ForgeAdmin!
Performance Note
Publishing with many subscribers (100+) can become a bottleneck. If your event bus triggers heavy computation, consider scheduling notifications as microtasks or using a message queue for async processing.
Production Insight
The biggest production issue with Observer: unbounded callback arrays. If your app publishes events faster than subscribers process them, the subscriber list grows. Worse, if a component crashes before unsubscribing, its callback remains and may operate on stale DOM elements.
Another failure: circular event loops — A updates B, B updates A, causing infinite recursion. Always guard against reentrant calls.
Rule: always return an unsubscribe function from subscribe(). Use a Set instead of Array to prevent duplicate subscriptions. Subscribe in component mount, unsubscribe in unmount.
Key Takeaway
Observer = notify many without them knowing each other.
Always unsubscribe to avoid memory leaks.
Guard against circular updates.
Observer vs Pub/Sub vs Direct Call
IfOne sender, many receivers that shouldn't know each other
UseUse Observer pattern (Subject maintains list of Observers directly).
IfLoose coupling between publisher and subscriber via a broker
UseUse Pub/Sub (Event Bus) — publisher and subscriber never directly interact.
IfOnly one receiver needed, or callers know each other
UseDon't use Observer — just call the function directly. Simpler and faster.
Subject-Observer Relationship
notifiesnotifiesnotifiesSubjectObserver1Observer2Observer3

The Module Pattern: Encapsulation Before ES Modules

The Module pattern was the go-to way to create private scope before ES2015 modules. It uses an IIFE (Immediately Invoked Function Expression) to return a public API while keeping internal variables hidden. Even now, understanding it helps you read older codebases and makes you appreciate what import/export gives you.

The key insight: closures provide true private state. No prefix conventions like _private needed — the variable simply isn't accessible from outside. But the downside: each module creates its own closure, consuming memory for the lifetime of the module. And if you need to share a module across files, you need a script tag loader or a bundler.

Today, most teams use ES modules instead. But the Module pattern is still useful for inline scripts or when you can't use a bundler.

io/thecodeforge/patterns/ForgeModule.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * io.thecodeforge - Module Pattern (IIFE)
 */
const ForgeCounter = (function () {
    // Private
    let count = 0;

    function increment() {
        count++;
    }

    function decrement() {
        count--;
    }

    // Public API
    return {
        add: function (amount) {
            count += amount;
        },
        getCount: function () {
            return count;
        },
        reset: function () {
            count = 0;
        }
    };
})();

ForgeCounter.add(5);
console.log(ForgeCounter.getCount()); // 5
console.log(ForgeCounter.count); // undefined (private)
Output
5
Modern Alternative
In 2026, reach for ES modules first. But the Module pattern lives on in bundler configurations, inline scripts, and legacy codebases. Knowing it helps you understand closures and IIFEs at a deeper level.
Production Insight
The Module pattern's closure keeps variables alive as long as the module reference exists. If you accidentally create many modules in a loop (e.g., in a factory), you may retain large objects indefinably. Debugging is harder because you can't inspect private variables from DevTools.
Performance tip: if the module doesn't need internal state, export plain functions from a file — that's just a single scope, not a closure.
Rule: prefer ES modules for new code. Only use the Module pattern when you need true privacy without a build step.
Key Takeaway
Module pattern uses closure for privacy.
ES modules are the modern replacement.
Private state in a closure is memory that lives as long as the module does.

The Strategy Pattern: Swap Algorithms at Runtime

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The calling code uses a common interface, so you can swap strategies without touching the caller. This is invaluable for things like payment gateways, sorting algorithms, or validation rules that differ by user type or region.

In JavaScript, strategies are often just functions or objects with a common method signature. The pattern becomes especially powerful when combined with dependency injection or configuration files.

Common trap: creating a new strategy for every minor variation. If the variation is just a parameter, a simple function with a parameter is cleaner. Strategy is for when the entire behaviour changes, not just a value.

io/thecodeforge/patterns/ForgeStrategy.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
 * io.thecodeforge - Strategy Pattern for Payment
 */
class PaymentContext {
    constructor(strategy) {
        this.strategy = strategy;
    }

    setStrategy(strategy) {
        this.strategy = strategy;
    }

    executePayment(amount) {
        return this.strategy.pay(amount);
    }
}

const stripeStrategy = {
    pay(amount) { return `Paid $${amount} via Stripe`; }
};

const paypalStrategy = {
    pay(amount) { return `Paid $${amount} via PayPal`; }
};

const payment = new PaymentContext(stripeStrategy);
console.log(payment.executePayment(100));
payment.setStrategy(paypalStrategy);
console.log(payment.executePayment(50));
Output
Paid $100 via Stripe
Paid $50 via PayPal
Strategy as Interchangeable Parts
  • Context delegates to the current strategy
  • Strategies implement a common interface (same method signature)
  • You can swap strategies at runtime or at construction
  • Adding a new strategy doesn't change existing code (Open/Closed Principle)
  • Don't use Strategy if you only need a simple parameter switch
Production Insight
Production risk: switching strategies mid-transaction can lead to inconsistency. For example, you start a payment with Stripe, then switch to PayPal before completing — don't do that. Strategy swap should happen between business transactions, not within them.
Another issue: strategies that have different setup requirements (e.g., Stripe needs an API key, PayPal needs a token). The context shouldn't know about those details — but it often leaks through configuration.
Rule: keep strategies stateless when possible. If a strategy needs state, pass it via the pay() method, not through the constructor.
Key Takeaway
Strategy = interchangeable algorithms.
Keep strategies stateless.
Don't overuse — a switch statement is sometimes enough.
Strategy vs Simple Function Parameter
IfThe algorithm varies by an enum or a few flags
UseUse a simple switch/object lookup inside one function — no need for Strategy pattern.
IfEach algorithm has completely different logic and setup
UseUse Strategy pattern. Encapsulate each algorithm in its own class/module.
IfYou need to add new algorithms without modifying existing code
UseStrategy is the right choice. It's the O in SOLID (Open/Closed).

Classic Design Patterns Quick Reference Table

The following table categorizes the classic Gang of Four design patterns into Creational, Structural, and Behavioral types, with brief descriptions and typical use cases in JavaScript.

PatternCategoryDescriptionUse Case
SingletonCreationalEnsures a class has only one instanceGlobal config, logging
Factory MethodCreationalDefines an interface for creating an object, but lets subclasses decide which class to instantiateCreating UI components based on type
Abstract FactoryCreationalProvides an interface for creating families of related or dependent objectsCross-platform UI toolkit
BuilderCreationalSeparates construction of a complex object from its representationBuilding complex objects like HTTP requests
PrototypeCreationalCreates new objects by copying an existing object (clone)Object replication when construction is expensive
AdapterStructuralAllows incompatible interfaces to work togetherWrapping legacy API for modern interface
DecoratorStructuralAttaches additional responsibilities to an object dynamicallyAdding logging, validation, caching to functions
FacadeStructuralProvides a simplified interface to a complex subsystemAPI gateway pattern, DB abstraction
ProxyStructuralProvides a surrogate or placeholder for another object to control accessLazy loading, caching, access control
ObserverBehavioralDefines a one-to-many dependency between objectsEvent systems, real-time updates
StrategyBehavioralDefines a family of algorithms, encapsulates each one, and makes them interchangeablePayment methods, sorting algorithms
CommandBehavioralEncapsulates a request as an objectUndo/redo, task queues
IteratorBehavioralProvides a way to access elements of an aggregate object sequentiallyCustom collection traversal
MediatorBehavioralDefines an object that encapsulates how a set of objects interactChat room, event bus
StateBehavioralAllows an object to alter its behavior when its internal state changesState machines (login states, order states)
Template MethodBehavioralDefines the skeleton of an algorithm in a method, deferring some steps to subclassesCode generation, game AI
VisitorBehavioralRepresents an operation to be performed on elements of an object structureAST traversal, export operations

This table provides a high-level overview. In modern JavaScript development, some patterns are less common due to native language features (e.g., Iterator pattern is built-in with generators). Others like Observer and Strategy remain essential.

Production Insight
When choosing a pattern, consider the maintenance cost. Patterns that add many classes (like Visitor) can be overkill in JavaScript without static type checking. Prefer composition over inheritance and use patterns that align with JavaScript's prototypal nature.
Key Takeaway
The 23 classic GoF patterns are still relevant, but many can be replaced by simpler language features or modern frameworks.

Modern JS Alternatives: Hooks, Context & ES Modules

Classic design patterns were conceived in an era of class-heavy OOP languages. JavaScript has evolved significantly since the GoF book. Modern language features can simplify or outright replace several patterns:

  • Singleton → Module-level state + import singleton: Instead of a class with instance control, just export an object literal or use a module-scoped variable. Node.js modules are singletons by default.
  • Observer → React Hooks (useEffect + custom events): The Observer pattern is built into React's state system. Instead of a custom event bus, use useEffect for subscriptions and useCallback to memoize callbacks. For cross-component communication, the Context API + useReducer can replace a global event bus.
  • Strategy → Higher-order functions: Instead of creating separate Strategy classes, pass functions directly. React's useEffect dependency array and function composition achieve the same goal.
  • Factory → Component factories with JSX: In React, you can create factory functions that return JSX elements. Dynamic rendering is straightforward.
  • Command → Async queues with Promises: The Command pattern for undo/redo can be implemented with a simple array of state snapshots or using libraries like Immer for immutable state.

However, understanding the original patterns is crucial even when using modern alternatives. The vocabulary helps you communicate architectural decisions, and the patterns' pitfalls (like memory leaks in Observer) still apply to modern implementations.

io/thecodeforge/patterns/ModernAlternatives.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Singleton alternative: module-level export
// config.js
export const config = { theme: 'dark' }; // singleton per module

// Observer alternative: React Hook
import { useEffect, useState } from 'react';
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  useEffect(() => {
    const update = () => setIsOnline(navigator.onLine);
    window.addEventListener('online', update);
    window.addEventListener('offline', update);
    return () => {
      window.removeEventListener('online', update);
      window.removeEventListener('offline', update);
    };
  }, []);
  return isOnline;
}

// Strategy alternative: function argument
function paymentStrategy(amount, method) {
  const strategies = {
    stripe: (amt) => `Stripe: $${amt}`,
    paypal: (amt) => `PayPal: $${amt}`,
  };
  return strategies[method](amount);
}
console.log(paymentStrategy(100, 'stripe'));
Output
Stripe: $100
When to Use Classic vs Modern
Classic patterns still shine in backend Node.js code where frameworks are less opinionated. In frontend frameworks like React, many patterns are baked in. Use classic patterns when you need framework-agnostic design or when the pattern's formal structure brings clarity to a complex system.
Production Insight
Modern alternatives often reduce boilerplate but can hide performance pitfalls. For example, Context API can cause unnecessary re-renders if not properly memoized. Always profile before adopting a pattern, whether classic or modern.
Key Takeaway
Modern JS features can replace many classic patterns, but the underlying architectural principles remain. Use patterns that fit your framework and runtime constraints.

The Decorator Pattern: Wrapping Behavior Without Inheritance

You need to add logging to an API client, but you can't touch the base class because it's in a third-party library. Inheritance breaks. The Decorator pattern lets you wrap an object with additional behavior at runtime. You pass the original object into a wrapper that implements the same interface, then delegate calls with extra logic. In ES2024, this pattern maps directly to higher-order functions and Proxy objects. Don't build a class hierarchy for every cross-cutting concern. Decorate instead. Your test suite stays clean because you can mock the original object and test the decorator in isolation. The WHY: inheritance couples you to a fixed tree of behavior at compile time. Decorators compose behavior at runtime, one wrapper at a time.

apiDecorator.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge
class FetchUsers {
  async execute() {
    const res = await fetch('/api/users');
    return res.json();
  }
}

function withLogging(service) {
  return {
    execute: async (...args) => {
      console.log(`[${Date.now()}] calling execute`);
      const result = await service.execute(...args);
      console.log(`[${Date.now()}] result size: ${result.length}`);
      return result;
    }
  };
}

const loggedFetch = withLogging(new FetchUsers());
await loggedFetch.execute();
Output
[1710000000000] calling execute
[1710000000500] result size: 42
Production Trap:
Decorators that mutate the original object (like monkey-patching) break stack traces and debugger clarity. Always return a new wrapper. That way, you can compose multiple decorators without side effects.
Key Takeaway
Wrap, don't modify. Test the wrapper, mock the core.

The Prototype Pattern: Cloning State Instead of Rebuilding

You've got a complex configuration object with 15 nested fields. Every new request needs a slightly tweaked copy. Don't call a constructor with 20 parameters. The Prototype pattern says: clone an existing object, then mutate only what you need. JavaScript already implements this with Object.create() and the spread operator. For deep clones, structuredClone() (ES2024) handles circular references and typed arrays without third-party libs. The WHY: object construction is expensive when you have deep defaults or runtime-derived state. Cloning preserves that state without re-initializing. Use this when your object creation involves I/O, heavy computation, or complex defaults. But watch out — shallow clones create aliasing bugs that burn production.

configPrototype.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge
const defaultConfig = {
  apiBase: 'https://api.example.com',
  retryPolicy: { maxRetries: 3, backoffMs: 1000 },
  timeout: 5000
};

function createClientConfig(overrides = {}) {
  return structuredClone({
    ...defaultConfig,
    ...overrides
  });
}

const stagingConfig = createClientConfig({ apiBase: 'https://staging.example.com' });
stagingConfig.retryPolicy.maxRetries = 5; // Only affects staging, not default

console.log(defaultConfig.retryPolicy.maxRetries); // 3 — safe!
Output
3
Production Trap:
Shallow clone with spread operator shares nested object references. Your 'override' silently corrupts the prototype. Always use structuredClone() or deep-merging libraries for nesting deeper than one level.
Key Takeaway
Clone deep, mutate shallow. structuredClone() is your production-grade copier.
● Production incidentPOST-MORTEMseverity: high

The Singleton That Held State Across Tests

Symptom
Tests passed in isolation but failed when run together. A test expecting 'dark' theme got 'light' because a previous test had mutated the singleton's settings.
Assumption
The team assumed a new test runner instance meant a clean state for each test file.
Root cause
The singleton was loaded once per Node.js process. The constructor returned the same instance repeatedly, preserving mutations across test files.
Fix
Added a static reset method: static reset() { this.#instance = null; } and called it in beforeEach hooks. Used a WeakMap to detect test environment and force reset.
Key lesson
  • Singletons in Node.js are per-process, not per-request. Always provide a way to clear state in test environments.
  • Never assume a fresh import gives a fresh instance — Node module cache ensures the singleton lives as long as the process.
  • Prefer dependency injection over direct singleton access in code that needs to be testable.
Production debug guideSymptom → Action guide for pattern-related failures4 entries
Symptom · 01
A global config object changes unexpectedly between requests
Fix
Check if the config is a singleton and if any code mutates it directly. Add Object.freeze() to prevent accidental mutations.
Symptom · 02
UI components react to state changes they shouldn't
Fix
Verify observer/subscriber lists — look for duplicate subscriptions or missing unsubscribes. Use WeakRef or FinalizationRegistry for auto-cleanup.
Symptom · 03
Factory returns wrong type of object after a code change
Fix
Check the mapping logic in the factory. Add type-checking with instanceof or branded types. Write unit tests for every known type.
Symptom · 04
Memory grows continuously in a long-running server
Fix
Profile with Chrome DevTools heap snapshot. Look for retained event listeners or observer subscribers that were never removed.
★ Pattern Debugging Quick ReferenceCommon pattern misconfigurations and immediate fixes
Singleton state persists between requests in Node.js
Immediate action
Add a reset method and call it in request middleware (be careful with async contexts).
Commands
Add `static reset() { this.#instance = null; }`
Use `AsyncLocalStorage` to isolate request-scoped state.
Fix now
Wrap the singleton access in a factory that checks AsyncLocalStorage first.
Observer listener fires after component is destroyed+
Immediate action
Search for missing `unsubscribe` calls in component cleanup (e.g., `useEffect` return).
Commands
Add a WeakMap to track subscriptions per component instance.
Use a subscription manager that removes listeners when the component triggers finalisation.
Fix now
Implement a useEventBus hook that returns an unsubscribe function in the cleanup.
Factory creates an object that doesn't have a required method+
Immediate action
Enforce interface contracts using TypeScript or runtime checks.
Commands
Add a switch/case unit test that validates every branch returns the correct shape.
Use a proxy to validate returned objects at test time.
Fix now
Export a type guard function for each product type.
Design Pattern Comparison
PatternCategoryPrimary Use Case
SingletonCreationalShared state/resource (e.g., Database Pool, Config)
FactoryCreationalCreating objects without exposing creation logic
ObserverBehavioralEvent-driven updates (e.g., UI updates, WebSockets)
ModuleStructuralEncapsulation and private/public scope management
StrategyBehavioralSwapping algorithms at runtime (e.g., Payment gateways)

Key takeaways

1
Design patterns are templates for solving architectural problems, not rigid rules.
2
Creational patterns (Factory, Singleton) focus on efficient and controlled object instantiation.
3
Behavioral patterns (Observer, Strategy) manage communication between disparate objects.
4
Modern JS syntax (Private fields #, static methods) makes implementing classic patterns much cleaner and more secure.
5
Always favor readability over pattern purity—patterns should simplify your code, not complicate it.

Common mistakes to avoid

5 patterns
×

Over-Engineering with Factory

Symptom
A Factory method is created for a class that only has one implementation. Codebase has LoggerFactory.getLogger() returning the same Logger every time.
Fix
Remove the factory and export the class directly. Only introduce a factory when you actually need to vary the product type.
×

Singleton Global State Abuse

Symptom
Unit tests fail randomly because the singleton's state persists between tests. A test that expects 'dark' mode gets 'light' because a previous test changed the setting.
Fix
Add a static reset method and call it in beforeEach or setup. Better: inject the singleton via constructor so tests can provide a mock.
×

Memory Leaks in Observer Subscriptions

Symptom
Memory grows continuously in a long-running SPA. Heap snapshots show thousands of retained event listeners from unmounted components.
Fix
Always unsubscribe in the cleanup phase of component lifecycle (React: useEffect return, Angular: ngOnDestroy). Use WeakRef or a subscription manager with automatic cleanup.
×

Ignoring Native ES Modules

Symptom
Developers manually implement the Module pattern with IIFEs in a modern project where import/export is available. Increases bundle size and complexity.
Fix
Switch to ES modules (import/export). They provide proper encapsulation, tree-shaking, and better tooling support.
×

Strategy Pattern for Simple Logic

Symptom
A payment system has a Strategy class for each payment method, but the only difference is a URL and API key. Adds unnecessary class hierarchy.
Fix
Use a config object or a simple switch/if-else inside a single function. Only promote to Strategy when the algorithms differ significantly in logic.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the 'Open/Closed Principle' and which design pattern helps imple...
Q02SENIOR
How would you implement a Thread-Safe Singleton in a Node.js environment...
Q03SENIOR
Design a 'Undo/Redo' system using the Command Pattern in JavaScript.
Q04SENIOR
What are the pros and cons of using the Factory Pattern versus a simple ...
Q05SENIOR
How do you prevent memory leaks when implementing the Observer pattern i...
Q06SENIOR
Explain the Strategy Pattern and provide a code example for handling mul...
Q01 of 06SENIOR

Explain the 'Open/Closed Principle' and which design pattern helps implement it best.

ANSWER
The Open/Closed Principle states that classes should be open for extension but closed for modification. The Strategy pattern is a classic example: you can add new strategies (e.g., new payment gateway) without modifying existing code — just create a new class that implements the common interface. The Factory pattern also supports it by letting you add new product types without changing the caller.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Are design patterns still relevant in modern React/Vue development?
02
Is the Singleton pattern an Anti-Pattern?
03
What is the difference between the Observer and Pub-Sub patterns?
04
How do I implement a Private Constructor in JavaScript for a Singleton?
05
What pattern should I use for complex form validation with different rules per field?
06
How do design patterns affect performance in JavaScript?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Advanced JS. Mark it forged?

9 min read · try the examples if you haven't

Previous
Memoisation in JavaScript
17 / 27 · Advanced JS
Next
Functional Programming in JS