Skip to Content
Course content

157: The Singleton Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

At some point in your project, you're going to realize you have a piece of state that absolutely must be the same everywhere in your application. I usually see this happen with things like a database connection pool, a global configuration object, or a user session manager. The last thing you want is three different parts of your app holding three different versions of your "global" settings because you accidentally instantiated the manager class three times.

The chaos of multiple instances

The naive way to handle this is to just create a class and instantiate it wherever you need it. It feels natural—that's how we're taught OOP—but it creates a synchronization nightmare. Imagine we're building a ConfigurationManager to handle API keys and theme settings.

class ConfigurationManager {
  constructor() {
    this.settings = { theme: 'light', apiVersion: 'v1' };
  }

  setSetting(key, value) {
    this.settings[key] = value;
  }

  getSetting(key) {
    return this.settings[key];
  }
}

// In file A.js
const configA = new ConfigurationManager();
configA.setSetting('theme', 'dark');

// In file B.js
const configB = new ConfigurationManager();
console.log(configB.getSetting('theme')); // 'light' - Wait, what?

The problem here is that configA and configB are entirely different objects in memory. You've essentially created two separate sources of truth. If you're trying to change the theme of your entire app, you'll find yourself wondering why half your components are still in light mode. You could try to pass the instance around as a prop or argument through ten different layers of your app, but that's just "prop drilling" and it makes your code brittle and tedious to maintain.

The classic Singleton approach

To fix this, we want to ensure that no matter how many times we *try* to create a new instance, we always get back the exact same one. The traditional way to do this is to hijack the constructor. I'll be honest: this is the way you'll see it in Java or C# textbooks, and while it works in JS, it feels a bit clunky.

class ConfigurationManager {
  constructor() {
    if (ConfigurationManager.instance) {
      return ConfigurationManager.instance;
    }
    this.settings = { theme: 'light', apiVersion: 'v1' };
    ConfigurationManager.instance = this;
  }

  setSetting(key, value) {
    this.settings[key] = value;
  }

  getSetting(key) {
    return this.settings[key];
  }
}

const configA = new ConfigurationManager();
const configB = new ConfigurationManager();
console.log(configA === configB); // true

By checking for a static instance property, the constructor effectively says, "If I've already been built once, just give them the old one." It solves the problem, but it's a bit deceptive. When you call new, you expect a new object, not a cached one. It's a "magic" behavior that can confuse other developers on your team.

The idiomatic JavaScript way

In modern JavaScript, we have a much cleaner tool for this: the module system. ES modules are cached the first time they are imported. This means if you instantiate your class inside the module and export that specific instance, Node.js or the browser will ensure that every other file importing that module gets the same instance. It's simpler, more explicit, and removes the need for weird constructor logic.

// configManager.js
class ConfigurationManager {
  constructor() {
    this.settings = { theme: 'light', apiVersion: 'v1' };
  }

  setSetting(key, value) {
    this.settings[key] = value;
  }

  getSetting(key) {
    return this.settings[key];
  }
}

// We instantiate it here, once.
const instance = new ConfigurationManager();
export default instance;

Now, in any other file, you just import config from './configManager.js'. You aren't calling new anymore; you're just grabbing the existing instance. I prefer this because it treats the singleton as a shared resource rather than a trick of the constructor. You get the guarantee of a single source of truth without the "magic" that makes debugging a headache.




📋 Practical Task

Build a Shared AppState Singleton

You are building a small application that needs to track a global AppState (containing the current currentPage and isLoggedIn status) across multiple files.

Your task is to implement this using the idiomatic JavaScript module pattern. Create a AppState class with methods to updateState(key, value) and getState(key). Ensure that when you export the state, any file importing it interacts with the exact same instance. To verify your work, simulate two different modules importing the state: have the first module update the currentPage to "Dashboard", and have the second module log that value to the console to prove they are sharing the same object.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.