DEV Community

Cover image for JavaScript Singleton Pattern: From Theory to Practice
JSDev Space
JSDev Space

Posted on

JavaScript Singleton Pattern: From Theory to Practice

The Singleton is one of those design patterns that looks almost too simple at first. You create an object once, keep it somewhere, and return the same object whenever the application needs it again.

The implementation is small. The interesting part is understanding when that restriction is actually useful and when a regular object, module, or dependency would be a better choice.

In this guide, we'll build a Singleton in modern JavaScript, test how it behaves, and look at a practical example.

What Is the Singleton Pattern?

The Singleton is one of the original 23 design patterns described by the Gang of Four.

Its basic rule is simple:

Create a single instance and provide a consistent way to access it.

Imagine an application with a shared service that keeps some state. Creating several independent copies of that service could lead to duplicated resources or conflicting state.

A Singleton gives the application one shared instance instead.

Typical examples include:

Use case Why a single instance can help
Application state Several parts of the app need access to the same state
Modal or window manager One object coordinates opening and closing UI elements
Cache Different consumers should read and update the same cached data
Configuration The application uses one shared configuration object
Shared service One object manages a resource used across the application

Database connections are also frequently mentioned when discussing Singletons, although real applications usually use a connection pool managed by a library rather than implementing a database Singleton manually.

Why Would You Need a Singleton?

Suppose we have a class that manages a popup:

class Popup {
  open(url) {
    window.open(url, "_blank");
  }
}

const firstPopup = new Popup();
const secondPopup = new Popup();

console.log(firstPopup === secondPopup); // false
Enter fullscreen mode Exit fullscreen mode

Every call to new Popup() creates another object.

For a tiny class like this, that isn't necessarily a problem. But imagine that the object also stores configuration, event listeners, DOM references, cached data, or other shared state.

Now different parts of the application could end up working with different instances:

const firstPopup = new Popup();
const secondPopup = new Popup();

// Two independent objects
console.log(firstPopup === secondPopup); // false
Enter fullscreen mode Exit fullscreen mode

Sometimes that's exactly what you want. Sometimes it isn't.

If the application is supposed to have one central popup manager, creating multiple managers makes the architecture harder to reason about. A Singleton is one way to enforce that shared instance.

A Basic Singleton in JavaScript

A straightforward class-based implementation uses a static field and a static method:

class Popup {
  static instance;

  static getInstance() {
    if (!Popup.instance) {
      Popup.instance = new Popup();
    }

    return Popup.instance;
  }

  open(url) {
    window.open(url, "_blank");
  }
}
Enter fullscreen mode Exit fullscreen mode

There are three important pieces here.

1. A static field stores the instance

static instance;
Enter fullscreen mode Exit fullscreen mode

Static fields belong to the class itself rather than to objects created from the class.

That makes Popup.instance a convenient place to keep the shared instance.

2. The instance is created when it is first needed

if (!Popup.instance) {
  Popup.instance = new Popup();
}
Enter fullscreen mode Exit fullscreen mode

The first call creates the object.

Later calls skip this block because Popup.instance already exists.

This technique is usually called lazy initialization.

3. getInstance() becomes the access point

Instead of doing this:

const popup = new Popup();
Enter fullscreen mode Exit fullscreen mode

code that follows this pattern does this:

const popup = Popup.getInstance();
Enter fullscreen mode Exit fullscreen mode

Every caller receives the same stored object.

Checking That It Works

Let's create two variables:

const first = Popup.getInstance();
const second = Popup.getInstance();

console.log(first === second); // true
Enter fullscreen mode Exit fullscreen mode

Both variables point to the same object.

Conceptually, getInstance() follows this path:

Popup.getInstance()
        |
        v
Does an instance exist?
        |
   +----+----+
   |         |
  No        Yes
   |         |
   v         v
Create     Return
Popup      existing
   |
   v
Store instance
   |
   v
Return it
Enter fullscreen mode Exit fullscreen mode

The first call creates the instance. Every call after that returns what was already created.

Making the Singleton Harder to Bypass

There is a weakness in our first implementation.

Nothing prevents another developer from writing:

const popup = new Popup();
Enter fullscreen mode Exit fullscreen mode

That creates another instance and bypasses getInstance() completely.

JavaScript doesn't currently give us the same private-constructor mechanism available in languages such as Java or C#. Still, we can make accidental misuse more difficult.

One option is to let the constructor return the existing instance:

class Popup {
  static #instance;

  constructor() {
    if (Popup.#instance) {
      return Popup.#instance;
    }

    Popup.#instance = this;
  }

  static getInstance() {
    return Popup.#instance ?? new Popup();
  }

  open(url) {
    window.open(url, "_blank");
  }
}
Enter fullscreen mode Exit fullscreen mode

Now even direct construction returns the same object:

const first = new Popup();
const second = new Popup();

console.log(first === second); // true
Enter fullscreen mode Exit fullscreen mode

This is stronger than relying on convention alone, although it also makes the constructor's behavior less obvious.

For many projects, keeping construction simple and controlling how the class is exported is easier to maintain.

Practical Example: A Shared Window Manager

Let's use the pattern in a small page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta
    name="viewport"
    content="width=device-width, initial-scale=1"
  >
  <title>Singleton Window Manager</title>
</head>

<body>
  <button id="openBtn">Open JavaScript Development</button>

  <script>
    class WindowManager {
      static #instance;

      static getInstance() {
        if (!WindowManager.#instance) {
          WindowManager.#instance = new WindowManager();
        }

        return WindowManager.#instance;
      }

      open(url) {
        window.open(url, "_blank", "noopener,noreferrer");
      }
    }

    const windowManager = WindowManager.getInstance();
    const openButton = document.querySelector("#openBtn");

    openButton.addEventListener("click", () => {
      windowManager.open("https://jsdevspace.substack.com/");
    });
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The page creates access to the manager once:

const windowManager = WindowManager.getInstance();
Enter fullscreen mode Exit fullscreen mode

Other parts of the application can call getInstance() again without creating another manager.

const anotherReference = WindowManager.getInstance();

console.log(
  windowManager === anotherReference
); // true
Enter fullscreen mode Exit fullscreen mode

Both variables reference the same object.

You Don't Always Need a Class

There is an important JavaScript-specific detail that often gets lost when Singleton examples are copied from Java or C++.

ES modules are evaluated once and then cached.

That means JavaScript already gives us a natural way to share one object between modules.

For example:

// appStore.js

const appStore = {
  user: null,
  theme: "dark",

  setUser(user) {
    this.user = user;
  },

  setTheme(theme) {
    this.theme = theme;
  }
};

export default appStore;
Enter fullscreen mode Exit fullscreen mode

You can import it elsewhere:

import appStore from "./appStore.js";

appStore.setUser({
  id: 42,
  name: "Alex"
});
Enter fullscreen mode Exit fullscreen mode

And another module can import the same object:

import appStore from "./appStore.js";

console.log(appStore.user);
Enter fullscreen mode Exit fullscreen mode

Both modules receive the same exported object from that module instance.

In modern JavaScript applications, this is often simpler than building a traditional getInstance() class.

The class-based Singleton still matters because you'll encounter it in existing codebases, interviews, libraries, and systems where initialization needs more explicit control.

Lazy vs Eager Initialization

Our previous example creates the object only when somebody asks for it:

static getInstance() {
  if (!WindowManager.#instance) {
    WindowManager.#instance = new WindowManager();
  }

  return WindowManager.#instance;
}
Enter fullscreen mode Exit fullscreen mode

That's lazy initialization.

You can also create the instance immediately:

class Config {
  static instance = new Config();
}
Enter fullscreen mode Exit fullscreen mode

This is eager initialization.

Neither approach is automatically better.

Lazy initialization is useful when creating the object is expensive or it may never be needed. Eager initialization is simpler when the object is lightweight and the application always uses it.

Choose based on the lifetime and cost of the resource rather than treating lazy initialization as a requirement of the pattern.

Singleton State Is Global State

The biggest advantage of a Singleton is also its biggest risk.

Consider this service:

class Counter {
  static #instance;

  count = 0;

  static getInstance() {
    return Counter.#instance ??= new Counter();
  }

  increment() {
    this.count += 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

Two consumers share the same state:

const first = Counter.getInstance();
const second = Counter.getInstance();

first.increment();

console.log(second.count); // 1
Enter fullscreen mode Exit fullscreen mode

That can be convenient.

It also means that changing the object in one place affects every other consumer. Once a codebase grows, these invisible relationships can make debugging and testing more difficult.

A Singleton should therefore be used because shared identity is part of the design, not simply because global access is convenient.

Advantages of the Singleton Pattern

A Singleton can be useful when one shared instance genuinely represents the resource you're modeling.

It can reduce unnecessary object creation, provide a central access point, and make shared state straightforward.

There are practical cases where that fits naturally:

Application
    |
    +-- WindowManager
    |
    +-- Cache
    |
    +-- Configuration
Enter fullscreen mode Exit fullscreen mode

Each service exists once and is reused by different parts of the application.

Disadvantages

Singletons also introduce trade-offs.

The first is global state. Any code with access to the Singleton may be able to modify its state.

Testing can become more complicated too:

const store = Store.getInstance();

store.user = testUser;
Enter fullscreen mode Exit fullscreen mode

If that state survives between tests, one test may accidentally influence another unless the Singleton provides a reset mechanism or the test environment isolates modules.

Singletons can also create hidden dependencies. A function may look independent while quietly reaching into a globally accessible service.

Compare:

function savePost(post) {
  Database.getInstance().save(post);
}
Enter fullscreen mode Exit fullscreen mode

with explicit dependency injection:

function savePost(post, database) {
  database.save(post);
}
Enter fullscreen mode Exit fullscreen mode

The second version makes the dependency visible and usually makes the function easier to test.

This doesn't make Singletons inherently bad. It means they should solve a real architectural problem rather than become the default way to share objects.

Singleton vs Regular Instance

The distinction is easier to see side by side:

Regular class Singleton
new Service() creates another object Access returns a shared object
Instances can have independent state Consumers share state
Easy to create multiple configurations Designed around one identity
Dependencies can be passed explicitly Often accessed globally
Usually easier to isolate in tests May require additional test cleanup

If you need several independent instances, a Singleton is probably the wrong abstraction.

If there must logically be one shared coordinator or resource, it may be a good fit.

A More JavaScript-Friendly Alternative

Before creating a Singleton class, ask whether a module is enough.

Instead of this:

class Logger {
  static #instance;

  static getInstance() {
    return Logger.#instance ??= new Logger();
  }

  log(message) {
    console.log(message);
  }
}

export default Logger;
Enter fullscreen mode Exit fullscreen mode

you could simply export the service:

// logger.js

export function log(message) {
  console.log(message);
}
Enter fullscreen mode Exit fullscreen mode

Or export one configured object:

// logger.js

class Logger {
  log(message) {
    console.log(message);
  }
}

export const logger = new Logger();
Enter fullscreen mode Exit fullscreen mode

Then use it wherever it is needed:

import { logger } from "./logger.js";

logger.log("Application started");
Enter fullscreen mode Exit fullscreen mode

For many frontend and Node.js projects, this is the cleaner solution.

The traditional Singleton pattern becomes more useful when you need controlled initialization, internal state, lifecycle management, or compatibility with an architecture built around classes.


More JavaScript, without the filler

If you enjoy practical JavaScript articles like this one, I publish more tutorials, patterns, modern APIs, React techniques, and weekly development updates in JavaScript Development.

Subscribe to the JavaScript Development Substack to get new posts directly in your inbox:

https://jsdevspace.substack.com/


Final Thoughts

The Singleton pattern has one central idea: when an application needs one shared instance of something, control how that instance is created and accessed.

The traditional implementation looks like this:

class Service {
  static #instance;

  static getInstance() {
    return Service.#instance ??= new Service();
  }
}
Enter fullscreen mode Exit fullscreen mode

But knowing the syntax isn't the important part.

Before reaching for a Singleton, ask a more useful question: does this resource truly need one shared identity?

If the answer is yes, the pattern can provide a clear way to manage it. If all you need is a shared utility or object, JavaScript modules may already give you everything you need.

That's the difference between knowing how to implement a design pattern and knowing when it belongs in your code.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.