Join our WhatsApp Channel
Cart / 0.00$

No products in the cart.

Visit Support Portal
No Result
View All Result
Friday, November 6, 2026
  • Home
  • Tutorials
    • Machine Learning
    • Python
    • Web Development
    • Javascript
    • Mathematics
    • Deep Learning
    • Artificial Intelligence
  • Store
  • Our ServicesBook Now
    • Managed Hosting Services
    • Web Development Services
    • Digital Marketing Services
    • Website Security Checker
    • Video Editing Services
  • Our Softwares
    • JaggoRe AIFree
    • Email ExtractorPaid
    • TimeWell AppFree
Get a Free Quote
  • Login
  • Register
Neuraldemy
Friday, November 6, 2026
Cart / 0.00$

No products in the cart.

No Result
View All Result
Neuraldemy
Get A Free Quote
Home Web Development

Singleton Pattern In JavaScript/TypeScript

Amritesh Kumar by Amritesh Kumar
March 2, 2026 - Updated on October 23, 2026
in Web Development, Javascript, NodeJS, System Design
Reading Time: 94 mins read
A A
Singleton Pattern In JavaScript/TypeScript

Let’s learn about singleton pattern in JavaScript or more specifically Typescript. Imagine you are building a Node.js application that stores information in memory. One part of your application saves a user’s data, another part reads it, and a third part checks whether that data exists.

All three parts need access to the same cache.

At first, you might create a new cache wherever you need one. It seems reasonable, but something unexpected happens: each cache has its own collection of data. Information saved in one cache is not automatically available in another.

This leads us to an important question:

What if we wanted the entire application to share one particular instance of a class instead of creating multiple independent instances?

That is the problem the Singleton Pattern is designed to address.

The Singleton Pattern is a creational design pattern. A creational pattern deals with how objects are created. The Singleton Pattern controls access to a class instance so that, under the intended design, only one instance is created and reused within a particular scope.

In this tutorial, we will build that understanding from the ground up. We will first understand objects, instances, and the problem of creating multiple copies. Then we will explore the TypeScript concepts that make a singleton possible, including private, static, constructors, interfaces, and generics.

Finally, we will walk through an in-memory cache implementation line by line, follow the program’s execution, inspect its output, and discuss an important type-safety issue in the generic version of the code.

By the end, you should understand not only how to write a singleton, but also why it works, when it is useful, and where it can cause problems.

Table of Contents

  • 1. First Principles: What Is an Object Instance?
  • 2. The Problem: Multiple Objects Can Mean Multiple Copies of State
  • 3. What Exactly Is the Singleton Pattern?
  • 4. The TypeScript Concepts We Need Before Writing a Singleton
    • 4.1 What Is an Interface?
    • 4.2 What Does the Generic Type Parameter Mean?
    • 4.3 What Is a Constructor?
    • 4.4 What Does private Mean in TypeScript?
    • 4.5 What Does static Mean?
  • 5. Understanding the Data Structure: What Is a Map?
  • 6. Building the Singleton Cache: The Complete Implementation
  • 7. Step One: Define the Cache Contract with an Interface
    • The set() Method
    • The get() Method
    • The has() Method
    • The clear() Method
  • 8. Step Two: Define the InMemoryCache Class
  • 9. Step Three: Store a Reference to the Singleton
  • 10. Step Four: Make the Constructor Private
    • What Happens Inside the Constructor?
  • 11. Step Five: Understand getInstance()
    • Why Is It public?
    • Why Is It static?
    • Why Does It Check if (!InMemoryCache.instance)?
    • Why Does It Create the Instance Inside the Condition?
    • Why Does It Return the Instance?
  • 12. Step Six: Understand the Cache Methods
    • set(): Store a Value
    • get(): Retrieve a Value
    • has(): Check Whether a Key Exists
    • clear(): Remove All Cached Values
  • 13. Step Seven: Execute the Program and Follow the Flow
    • First Call: Create the Instance
    • Second Step: Store the User Value
    • Second Call: Request the Instance Again
    • Third Step: Read the Value Through the Second Variable
    • Fourth Step: Compare the Two Instances
  • 14. The Entire Singleton Flow in One Diagram
  • 15. An Important TypeScript Caveat: Is This Generic Singleton Fully Type-Safe?
    • Why Is That a Problem?
    • How Should We Handle It?
  • 16. Where Does a Singleton Actually Live?
  • 17. When Is the Singleton Pattern Useful?
  • 18. When Should You Avoid the Singleton Pattern?
    • Shared State Can Cause Unexpected Interactions
    • Tests Can Become Harder to Isolate
    • A Singleton Can Hide Dependencies
    • An In-Memory Singleton Is Not Persistent
  • 19. Singleton Pattern vs Module-Level Shared State
  • Conclusion: Understand the Problem, Not Just the Pattern
  • Further Reading

1. First Principles: What Is an Object Instance?

Before learning the Singleton Pattern, we need to understand what it means to create an instance of a class.

In TypeScript, a class describes how objects should be constructed and what behavior those objects should have.

Consider a simple class:

class User {
  constructor(public name: string) {}

  greet(): void {
    console.log(`Hello, ${this.name}!`);
  }
}

This class describes a User object. It has a name property and a greet() method.

Now create two objects:

const user1 = new User("Amritesh");
const user2 = new User("Riya");

user1.greet();
user2.greet();

console.log(user1 === user2);

Output:

Hello, Amritesh!
Hello, Riya!
false

What happened?

The new keyword creates an instance of the class. Each call to new User() creates a separate object.

Even though user1 and user2 are created from the same class, they are different instances. Each object has its own name property.

You can think of the class as a blueprint and the instances as the individual objects created from that blueprint.

A class defines what objects can do. An instance is a particular object created from that class.

This distinction is the starting point for understanding the Singleton Pattern.

2. The Problem: Multiple Objects Can Mean Multiple Copies of State

Let us create a basic in-memory cache.

A cache stores values so that the application can retrieve them later without calculating or loading them again. Our example will keep data in the application’s memory.

Here is a simple cache class:

class SimpleCache {
  private cache = new Map<string, string>();

  set(key: string, value: string): void {
    this.cache.set(key, value);
  }

  get(key: string): string | undefined {
    return this.cache.get(key);
  }
}

Do not worry about every line yet. We will examine Map, private, and the method signatures later.

For now, focus on how the class is used:

const cache1 = new SimpleCache();
const cache2 = new SimpleCache();

cache1.set("user_123", "Amritesh");

console.log(cache1.get("user_123"));
console.log(cache2.get("user_123"));

Output:

Amritesh
undefined

Why does the second lookup return undefined?

Because cache1 and cache2 are separate instances. Each instance creates its own Map, so adding an entry to one does not add it to the other.

The result is not a bug in JavaScript or TypeScript. It is exactly what we asked the language to do.

However, imagine that multiple parts of our application are supposed to share the same cache. Creating an independent instance in each part would defeat that goal.

We need a way to control how the cache instance is created.

Instead of allowing every part of the application to write:

const cache = new SimpleCache();

we want to provide a single shared instance through a method such as:

const cache = InMemoryCache.getInstance();

Calling getInstance() should return the same cache object each time.

That is the central idea behind the Singleton Pattern.

3. What Exactly Is the Singleton Pattern?

A Singleton Pattern organizes a class so that callers obtain its instance through a controlled access point rather than creating a new instance whenever they need one.

A typical singleton has three important characteristics.

First, it prevents ordinary callers from creating new instances directly. In TypeScript, a private constructor can help enforce this restriction.

Second, the class keeps track of an instance that can be reused.

Third, it provides a public method through which callers can obtain that instance.

The overall process looks like this:

Application requests the instance
               |
               v
      Does an instance exist?
          /           \
        No             Yes
        |               |
        v               |
 Create the instance    |
        |               |
        +-------+-------+
                |
                v
       Return the same instance

The first call creates the instance. Later calls return the one that already exists.

The instance is not recreated every time someone asks for it.

This is especially useful when multiple parts of an application must share one object, such as a particular in-memory cache, configuration manager, or application-level service.

However, a singleton does not automatically make every object in the entire computer share one instance. The scope matters. In a typical Node.js program, a singleton implemented with a static class property is shared through that class’s runtime definition. Separate processes, worker isolates, separately loaded class definitions, or independently created application contexts can have separate instances.

We will return to this limitation near the end.

4. The TypeScript Concepts We Need Before Writing a Singleton

Our cache implementation uses several TypeScript features. Understanding them first will make the final code much easier to follow.

4.1 What Is an Interface?

An interface describes the shape an object or class is expected to provide.

For example:

interface ICache<T> {
  set(key: string, value: T): void;
  get(key: string): T | undefined;
  has(key: string): boolean;
  clear(): void;
}

This interface describes the operations that a cache must support.

It says that the cache must provide four methods:

set() stores a value under a key.

get() retrieves the value associated with a key.

has() checks whether a key exists.

clear() removes all entries.

An interface describes what is expected, not how the methods are implemented.

For example, the interface does not say whether data should be stored in a Map, an array, or a database. Those are implementation decisions.

When a class declares:

class InMemoryCache<T> implements ICache<T> {
  // Implementation goes here
}

the implements ICache<T> clause tells TypeScript to check that the class provides the members required by that interface.

This helps catch mistakes while writing code. If you forget to implement one of the required methods, TypeScript can report a type error.

The I prefix in ICache is a naming convention some developers use to identify interfaces. TypeScript does not require interface names to begin with I.

4.2 What Does the Generic Type Parameter <T> Mean?

Look again at this interface:

interface ICache<T> {
  set(key: string, value: T): void;
  get(key: string): T | undefined;
}

What does T mean?

T is a generic type parameter. It lets us write one reusable definition that can work with different types of values.

Without a generic, we might create separate cache interfaces for strings, numbers, and user objects. That would duplicate a lot of code.

A generic interface allows this:

interface ICache<T> {
  set(key: string, value: T): void;
  get(key: string): T | undefined;
}

We can then use it with different types.

A cache whose values are strings can be represented as:

ICache<string>

A cache whose values are numbers can be represented as:

ICache<number>

A cache whose values are user objects can be represented as:

interface User {
  id: number;
  name: string;
}

ICache<User>

The same interface works for all three cases. The only difference is the value type supplied for T.

Consider a string cache:

const cache: ICache<string> = /* an implementation */;

cache.set("user_123", "Amritesh");

const user = cache.get("user_123");

TypeScript knows that values passed to set() should be strings. It also knows that get() returns either a string or undefined.

Why undefined? Because a requested key might not exist in the cache.

Generics allow us to reuse code while keeping its type information. They do not, by themselves, create separate runtime versions of a class for strings, numbers, or other types. That detail becomes important when we examine the generic singleton implementation.

4.3 What Is a Constructor?

A constructor is a special method that runs when an instance of a class is created.

Consider:

class Example {
  constructor() {
    console.log("An instance was created");
  }
}

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

Output:

An instance was created
An instance was created

The constructor runs once for each newly created instance.

A constructor is commonly used to initialize an object’s properties, create internal data structures, or prepare the object for use.

In our cache, the constructor will create a Map and print a message confirming that the cache has been initialized.

4.4 What Does private Mean in TypeScript?

The private keyword restricts access to a class member from ordinary external TypeScript code.

For example:

class Account {
  private balance = 100;
}

const account = new Account();

console.log(account.balance); // TypeScript error

The property exists, but TypeScript does not allow the caller to access it directly because it is declared private.

A key distinction is that TypeScript’s ordinary private modifier is primarily a compile-time access restriction. It is not the same as JavaScript’s runtime-enforced #privateField syntax.

In our singleton, we use private in two important places.

A private constructor() restricts callers from directly constructing instances through new.

A private static instance property keeps the singleton reference within the class’s accessible implementation.

Together, these restrictions make the intended object-creation path explicit.

4.5 What Does static Mean?

By default, class properties and methods belong to individual instances.

For example:

class Counter {
  count = 0;

  increment(): void {
    this.count++;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

counter1.increment();

console.log(counter1.count);
console.log(counter2.count);

Output:

1
0

Each instance has its own count property.

A static member works differently. It belongs to the class itself rather than to a particular instance.

For example:

class Application {
  static appName = "My Node App";
}

console.log(Application.appName);

Output:

My Node App

We access appName through Application, not through an object created with new Application().

This distinction is exactly what we need for the Singleton Pattern.

If the instance reference belonged to every individual object, every object would have its own instance reference, and that would not help us enforce a shared instance.

By declaring the reference as static, we keep it on the class itself.

There is one important TypeScript detail: a static member cannot directly use the class’s generic parameter T, because T belongs to an instance’s type while the static property belongs to the class constructor. Our original implementation uses any for its static reference to get around this limitation, which introduces a type-safety concern we will examine later.

5. Understanding the Data Structure: What Is a Map?

Our cache uses this declaration:

private cache: Map<string, T>;

To understand what it means, we need to learn about Map.

A Map is a JavaScript data structure that stores key-value pairs. Each key is associated with one value.

For example:

const users = new Map<string, string>();

users.set("user_123", "Amritesh");
users.set("user_456", "Riya");

console.log(users.get("user_123"));
console.log(users.has("user_456"));

Output:

Amritesh
true

The first lookup returns the value associated with "user_123". The second checks whether "user_456" exists.

A Map provides methods that make it a natural choice for a simple key-value cache:

set(key, value) adds a new entry or updates an existing one.

get(key) retrieves the value, returning undefined when no matching entry exists.

has(key) checks whether the key exists.

clear() removes all entries.

Now consider the following line from our class:

private cache: Map<string, T>;

It tells TypeScript that cache is a private property containing a Map whose keys are strings and whose values have the type T.

For a string cache, this becomes conceptually similar to:

Map<string, string>

For a number cache, it becomes conceptually similar to:

Map<string, number>

This is useful because it allows our cache to support different value types without changing the basic storage logic.

6. Building the Singleton Cache: The Complete Implementation

Now that we understand the building blocks, let us examine the Singleton Pattern implementation.

Here is the code:

// Singleton Pattern

interface ICache<T> {
  set(key: string, value: T): void;
  get(key: string): T | undefined;
  has(key: string): boolean;
  clear(): void;
}

class InMemoryCache<T> implements ICache<T> {
  private static instance: InMemoryCache<any>;
  private cache: Map<string, T>;

  private constructor() {
    this.cache = new Map<string, T>();
    console.log("Cache initialized in memory");
  }

  public static getInstance<T>(): InMemoryCache<T> {
    if (!InMemoryCache.instance) {
      InMemoryCache.instance = new InMemoryCache<T>();
    }

    return InMemoryCache.instance;
  }

  public set(key: string, value: T): void {
    this.cache.set(key, value);
  }

  public get(key: string): T | undefined {
    return this.cache.get(key);
  }

  public has(key: string): boolean {
    return this.cache.has(key);
  }

  public clear(): void {
    this.cache.clear();
  }
}

const myCache1 = InMemoryCache.getInstance<string>();

myCache1.set("user_123", "Amritesh");

const myCache2 = InMemoryCache.getInstance<string>();

console.log("Value from cache 2:", myCache2.get("user_123"));

console.log(
  "Are both caches the exact same instance?",
  myCache1 === myCache2
);

Expected output:

Cache initialized in memory
Value from cache 2: Amritesh
Are both caches the exact same instance? true

The result proves that, in this example, the second call returns the same object as the first call. That is why the value stored through myCache1 can be read through myCache2.

Let us break the code down into understandable pieces.

7. Step One: Define the Cache Contract with an Interface

We begin with:

interface ICache<T> {
  set(key: string, value: T): void;
  get(key: string): T | undefined;
  has(key: string): boolean;
  clear(): void;
}

The interface defines the operations our cache promises to provide.

It does not implement the storage mechanism. It simply establishes the contract that the class must satisfy.

Let us inspect each method.

The set() Method

set(key: string, value: T): void;

This method accepts a string key and a value of type T.

The return type is void, which means callers should not expect a meaningful return value.

For example, when T is string, this is allowed:

cache.set("user_123", "Amritesh");

But passing a number would be a type error if the cache is correctly typed as ICache<string>:

cache.set("user_123", 123); // TypeScript error

The get() Method

get(key: string): T | undefined;

This method retrieves the value stored under a key.

Why does it return T | undefined rather than just T?

Because the key might not exist. Returning undefined allows the caller to distinguish a missing entry from an available value.

For example:

const name = cache.get("user_123");

if (name !== undefined) {
  console.log(name);
}

This condition checks whether a value was found before using it.

The has() Method

has(key: string): boolean;

This returns true when the map contains the requested key and false otherwise.

The clear() Method

clear(): void;

This removes every entry from the cache.

Together, these methods define a simple interface that other parts of the program can use without needing to know the internal details of storage.

8. Step Two: Define the InMemoryCache Class

Next:

class InMemoryCache<T> implements ICache<T> {

This declares a class named InMemoryCache.

The <T> makes the class generic. Its values can be strings, numbers, objects, or another chosen type.

The implements ICache<T> clause asks TypeScript to verify that the class provides the methods required by the interface.

This separates two concerns. The interface describes what a cache should do, while the class describes how this particular cache does it.

The words InMemory also communicate an important implementation detail: our data is stored in process memory rather than a persistent database.

If the process exits, the in-memory values disappear unless they are saved somewhere else.

9. Step Three: Store a Reference to the Singleton

Now we reach a central line:

private static instance: InMemoryCache<any>;

There are three parts worth understanding.

private limits ordinary TypeScript access to the property from outside the class.

static means this property belongs to the InMemoryCache class itself, not to each cache object.

instance is the reference that will hold the shared cache object.

The type InMemoryCache<any> tells TypeScript that this static property refers to an InMemoryCache whose value type is treated as any.

Why use any here?

The class is generic, but static members belong to the class rather than to a particular generic instance. TypeScript does not let a static property directly use the class’s type parameter as though each generic instantiation had its own independent static field.

That choice allows this implementation to compile, but it weakens type safety. As we will see later, a caller can request a string cache and then request a number cache even though the code keeps just one shared instance. This is an important caveat rather than a detail to ignore.

For the moment, focus on the Singleton Pattern idea: the class has one shared reference that it can check before creating a new object.

10. Step Four: Make the Constructor Private

Look at the constructor:

private constructor() {
  this.cache = new Map<string, T>();
  console.log("Cache initialized in memory");
}

Normally, creating an object from a class requires calling its constructor through new:

const cache = new InMemoryCache<string>();

But our constructor is declared private.

That means code outside the class cannot normally construct an instance directly in TypeScript. The following attempt produces a TypeScript access error:

const cache = new InMemoryCache<string>(); // Error

Why is this restriction important?

If callers could freely create new instances, they could bypass the singleton access method and create multiple independent caches.

The private constructor directs callers toward the intended creation mechanism:

InMemoryCache.getInstance<string>();

Inside the class, the static method is allowed to call the private constructor, so the restriction does not prevent the class from creating its own instance.

What Happens Inside the Constructor?

The first line is:

this.cache = new Map<string, T>();

Here, this refers to the instance being initialized.

The line creates a new Map and assigns it to the instance’s cache property.

The map starts empty.

The next line is:

console.log("Cache initialized in memory");

This prints a message whenever the constructor runs.

Because the singleton method only creates an instance when none exists, this message should appear once during the normal execution of our example.

This log is useful for learning because it helps us see whether the constructor has run more than once.

11. Step Five: Understand getInstance()

Here is the most important method:

public static getInstance<T>(): InMemoryCache<T> {
  if (!InMemoryCache.instance) {
    InMemoryCache.instance = new InMemoryCache<T>();
  }

  return InMemoryCache.instance;
}

This method is the public entry point for obtaining the cache.

It combines the two key parts of the Singleton Pattern: checking whether an instance already exists and returning the existing instance when possible.

Let us examine the method line by line.

Why Is It public?

public static getInstance<T>()

public means that callers can access the method.

If this method were private, external application code would have no ordinary way to request the singleton.

The method is intentionally public because it is the route through which callers obtain the shared cache.

Why Is It static?

It must be possible to request the cache before having a cache object.

For example:

const cache = InMemoryCache.getInstance<string>();

Notice that we call the method through the class name, InMemoryCache, rather than through an existing object.

The method is static so that callers can access it directly through the class.

It then decides whether to create the object or return an existing one.

Why Does It Check if (!InMemoryCache.instance)?

if (!InMemoryCache.instance) {

This checks whether the class’s stored instance reference has a usable value.

Before any instance has been created, the property is undefined at runtime because no initializer was supplied.

Since undefined is falsy, the condition is true on the first call.

The class can then construct its instance.

After an instance has been assigned, the reference is truthy, so later calls skip the creation block.

Why Does It Create the Instance Inside the Condition?

InMemoryCache.instance = new InMemoryCache<T>();

This is the point at which the singleton is first constructed.

The constructor creates the internal Map, initializes the object, and prints:

Cache initialized in memory

The newly created object is then stored in the static instance property.

Because the property is static, subsequent calls through this class reference find the object created earlier.

This is often called lazy initialization. The object is created only when it is first requested, rather than being constructed immediately when the class is declared.

Why Does It Return the Instance?

return InMemoryCache.instance;

After the condition, the method returns the stored reference.

On the first call, the reference was just assigned to a newly created object.

On every later call, the method returns the object that was already stored.

That is the core behavior of this Singleton Pattern implementation.

12. Step Six: Understand the Cache Methods

The remaining methods operate on the same map.

set(): Store a Value

public set(key: string, value: T): void {
  this.cache.set(key, value);
}

The method accepts a key and a value. It passes both to the map’s set() method.

For example:

myCache1.set("user_123", "Amritesh");

The cache now contains a key-value pair:

"user_123" -> "Amritesh"

Notice that the method uses this.cache.

The this keyword refers to the object on which the method is being invoked. Since myCache1 and myCache2 refer to the same instance in our example, both use the same internal map.

get(): Retrieve a Value

public get(key: string): T | undefined {
  return this.cache.get(key);
}

This method asks the map for the value associated with the key.

If the key exists, the method returns the stored value. If the key does not exist, the method returns undefined.

For example:

console.log(myCache1.get("user_123"));

Output:

Amritesh

This works because the key was previously inserted into the map.

has(): Check Whether a Key Exists

public has(key: string): boolean {
  return this.cache.has(key);
}

This delegates to the Map‘s has() method.

For example:

console.log(myCache1.has("user_123"));
console.log(myCache1.has("user_999"));

Output:

true
false

The first key exists. The second does not.

clear(): Remove All Cached Values

public clear(): void {
  this.cache.clear();
}

Calling this method empties the map:

myCache1.clear();

All entries stored in that shared cache are removed.

Because a singleton is shared, this operation affects every part of the application that uses that same instance. That is useful when a global cache needs to be cleared, but it is also a responsibility: one part of the application can affect the cache contents seen by another.

13. Step Seven: Execute the Program and Follow the Flow

Now let us examine the usage code:

const myCache1 = InMemoryCache.getInstance<string>();

myCache1.set("user_123", "Amritesh");

const myCache2 = InMemoryCache.getInstance<string>();

console.log("Value from cache 2:", myCache2.get("user_123"));

console.log(
  "Are both caches the exact same instance?",
  myCache1 === myCache2
);

We will follow what happens in order.

First Call: Create the Instance

const myCache1 = InMemoryCache.getInstance<string>();

The program calls getInstance() for the first time.

Initially, the static instance property has no assigned object. The condition if (!InMemoryCache.instance) succeeds.

The class executes:

InMemoryCache.instance = new InMemoryCache<string>();

The constructor creates the map and prints:

Cache initialized in memory

The static property now holds a reference to the new object. The method returns that object, and myCache1 receives the reference.

Conceptually, we now have one cache object:

InMemoryCache.instance
          |
          v
      Cache Object
          |
          v
         Map

Second Step: Store the User Value

Next:

myCache1.set("user_123", "Amritesh");

The method inserts the key-value pair into the map owned by the object referenced by myCache1.

The cache now contains:

KeyValue
user_123Amritesh

The map belongs to the cache instance, not to the variable name myCache1. This distinction will matter when we obtain the instance through another variable.

Second Call: Request the Instance Again

Next:

const myCache2 = InMemoryCache.getInstance<string>();

The program calls getInstance() a second time.

This time, InMemoryCache.instance already references an object. Therefore, the condition:

if (!InMemoryCache.instance)

is false.

Node.js does not execute the constructor again. The method skips object creation and returns the stored reference.

Now both variables refer to the same object:

myCache1 ──────┐
               |
               v
           Cache Object
               ^
               |
myCache2 ──────┘

The variables have different names, but the underlying object is the same.

Third Step: Read the Value Through the Second Variable

Now consider:

console.log(
  "Value from cache 2:",
  myCache2.get("user_123")
);

The get() method reads from the map inside the object referenced by myCache2.

Because that object is the same object used by myCache1, it contains the value saved earlier.

The output is:

Value from cache 2: Amritesh

The important point is not that the variable names are different. The important point is that they reference the same cache object.

Fourth Step: Compare the Two Instances

Finally:

console.log(
  "Are both caches the exact same instance?",
  myCache1 === myCache2
);

The strict equality operator === compares object identity when used with objects.

For objects, this comparison is true when both operands refer to the same object, not merely when two separate objects contain identical properties.

Here, both variables reference the same object, so the result is:

Are both caches the exact same instance? true

If we had created two independent caches using new, the comparison would be false even if their contents happened to be identical.

14. The Entire Singleton Flow in One Diagram

Let us connect everything we have learned.

A simplified view of what happens each time getInstance() is called.

        Application
             |
             v
  InMemoryCache.getInstance()
             |
             v
  Is instance already stored?
       /             \
     No               Yes
     |                 |
     v                 |
 Call constructor       |
     |                 |
     v                 |
 Create Map             |
     |                 |
     v                 |
 Store static instance |
     |                 |
     +--------+--------+
              |
              v
     Return same object
              |
        +-----+-----+
        |           |
        v           v
     myCache1    myCache2
        |           |
        +-----+-----+
              |
              v
        Shared Map
              |
              v
   user_123 -> Amritesh

This illustrates the Singleton Pattern’s main promise in this implementation: there is one stored instance reference, and all successful calls through getInstance() return the same object.

15. An Important TypeScript Caveat: Is This Generic Singleton Fully Type-Safe?

There is a subtle issue in our code that is important to understand, especially if you plan to use this implementation in a real application.

The class declares:

class InMemoryCache<T> implements ICache<T> {
  private static instance: InMemoryCache<any>;

  public static getInstance<T>(): InMemoryCache<T> {
    if (!InMemoryCache.instance) {
      InMemoryCache.instance = new InMemoryCache<T>();
    }

    return InMemoryCache.instance;
  }
}

At first glance, this appears to support a separate, correctly typed singleton for any requested type.

For example:

const stringCache = InMemoryCache.getInstance<string>();
const numberCache = InMemoryCache.getInstance<number>();

But that is not what the implementation actually guarantees.

Why Is That a Problem?

The class contains one static property named instance.

The type parameter T does not create a separate runtime class definition for strings, numbers, or user objects. TypeScript generics provide type checking during development, but their type parameters are erased from the emitted JavaScript.

Consequently, the two calls above access the same static instance property.

Suppose the first call creates a cache used for strings:

const stringCache = InMemoryCache.getInstance<string>();

stringCache.set("user_123", "Amritesh");

Then a caller requests a number cache:

const numberCache = InMemoryCache.getInstance<number>();

The implementation does not create a new map for numbers. It returns the existing instance, while the return type tells TypeScript to treat that instance as an InMemoryCache<number>.

The problem becomes visible when reading values:

const value = numberCache.get("user_123");

// TypeScript believes value is number | undefined.
// But the actual stored value is the string "Amritesh".

The compiler believes that the value is a number or undefined, but the underlying map contains a string.

This is a type-safety problem caused by the combination of one shared instance and a generic getInstance<T>() method that allows every caller to choose a different type. The use of any makes the mismatch possible.

The lesson: A generic class does not automatically produce a different singleton for every type parameter.

How Should We Handle It?

The right solution depends on the behavior the application actually needs.

If the whole application needs one string cache, make that singleton explicitly string-specific. If the application needs different typed caches, give each one a deliberate identity and type rather than pretending that one shared instance can safely be any type on demand.

For our example, let us make the cache explicitly string-based.

interface ICache<T> {
  set(key: string, value: T): void;
  get(key: string): T | undefined;
  has(key: string): boolean;
  clear(): void;
}

class InMemoryStringCache implements ICache<string> {
  private static instance: InMemoryStringCache | undefined;
  private readonly cache = new Map<string, string>();

  private constructor() {
    console.log("String cache initialized in memory");
  }

  public static getInstance(): InMemoryStringCache {
    if (!InMemoryStringCache.instance) {
      InMemoryStringCache.instance = new InMemoryStringCache();
    }

    return InMemoryStringCache.instance;
  }

  public set(key: string, value: string): void {
    this.cache.set(key, value);
  }

  public get(key: string): string | undefined {
    return this.cache.get(key);
  }

  public has(key: string): boolean {
    return this.cache.has(key);
  }

  public clear(): void {
    this.cache.clear();
  }
}

const cache1 = InMemoryStringCache.getInstance();

cache1.set("user_123", "Amritesh");

const cache2 = InMemoryStringCache.getInstance();

console.log(cache2.get("user_123"));
console.log(cache1 === cache2);

Output:

String cache initialized in memory
Amritesh
true

This version makes the intended type explicit. The cache stores strings, and every caller receives the same string-cache instance. The generic getInstance<T>() method is gone, so a caller cannot ask that same singleton to pretend to be a number cache.

The trade-off is that this particular singleton handles strings only. If the application needs multiple independently typed caches, that should be designed explicitly rather than hidden behind a type parameter on one shared static instance.

16. Where Does a Singleton Actually Live?

It is tempting to hear “one instance” and imagine that the object exists only once everywhere in the world.

That is not what this pattern guarantees.

A singleton implemented with a static property is shared through the relevant class definition in a particular runtime context.

For example, a typical Node.js application can have several modules calling the same exported singleton factory. If they all reference the same class definition, they can receive the same instance.

However, separate Node.js processes have separate memory spaces. A singleton in one process is not automatically shared with another process.

Likewise, worker threads have separate JavaScript execution environments and do not automatically share ordinary JavaScript objects. They need an appropriate communication or shared-memory mechanism if data must cross worker boundaries.

This matters in real applications that use clustering, workers, serverless execution, or multiple application instances behind a load balancer.

A singleton is not a distributed cache. If several server processes need to share the same data, a dedicated shared service or data store, such as Redis or a database, may be more appropriate.

The scope of the Singleton Pattern should always match the scope of the state you want to share.

17. When Is the Singleton Pattern Useful?

Singletons can be useful when an application genuinely needs one shared object with controlled creation.

An in-memory cache is one example. Several parts of the application may need to retrieve data from the same cache rather than create unrelated copies.

An application configuration service is another example. If configuration is loaded once and then treated as read-only, multiple modules can use the same configuration object.

A logging service may also be designed as a shared instance when the application needs a common output destination and consistent logging configuration.

The common idea is that several parts of the system depend on one shared resource or one shared piece of state.

However, using a singleton does not automatically improve the design. If a class has no meaningful reason to restrict object creation, a normal class with explicit instances may be simpler.

18. When Should You Avoid the Singleton Pattern?

The Singleton Pattern can make code convenient, but it also introduces shared state and global access.

That can create problems when used without care.

Shared State Can Cause Unexpected Interactions

If several modules use the same cache, one module can change values that another module expects to remain unchanged.

For example, one component might call clear() while another component is relying on previously cached values.

Shared state can be useful, but its ownership and lifecycle need to be understood.

Tests Can Become Harder to Isolate

Suppose one test stores values in the singleton cache and does not clear them afterward.

A later test may receive the same cached instance and see leftover data. This can create tests that pass or fail depending on execution order.

The clear() method helps, but tests still need to manage shared state deliberately.

Dependency injection or passing an explicitly created cache to the components that need it can make testing easier.

A Singleton Can Hide Dependencies

Consider a function that quietly reaches into a global singleton to read configuration.

Its dependency may not be obvious from its parameters or constructor. This makes the function harder to understand in isolation.

Passing the required cache or configuration object explicitly can make dependencies clearer.

An In-Memory Singleton Is Not Persistent

The cache stores values in process memory. When the process restarts, the map starts empty.

If data needs to survive restarts, an in-memory singleton alone is not enough.

The broader lesson is that the Singleton Pattern is a tool for controlling object creation, not a universal solution for application architecture.

19. Singleton Pattern vs Module-Level Shared State

There is one final connection worth making in Node.js. Under normal circumstances, the first time a CommonJS module is loaded, Node.js evaluates it and caches the module’s exported value. Subsequent require() calls that resolve to the same cached module usually receive that cached export.

This means that a module can export a shared object without using a Singleton class.

For example:

// cache.ts

const cache = new Map<string, string>();

export default cache;

Other modules can import that same exported map:

import cache from "./cache.js";

cache.set("user_123", "Amritesh");

When the modules resolve to the same module instance, they share the exported map.

So why use the Singleton Pattern at all?

A class-based singleton provides an explicit creation method, a private constructor, and a place to group the behavior associated with the instance. That can be useful when the object requires initialization or has a larger interface.

A module-level shared object is simpler when all you need is one exported value and no additional creation logic.

These approaches are related, but they are not identical. A Singleton Pattern is a design technique for controlling instance creation, while module caching is a module-system behavior that can naturally preserve a shared exported object.

For a simple Node.js cache, the module approach might be enough. For learning object-oriented design patterns, implementing the Singleton Pattern helps explain how a class can control its own instance lifecycle.

Conclusion: Understand the Problem, Not Just the Pattern

The Singleton Pattern becomes much easier to understand when we begin with the underlying problem.

A class can create many independent objects. That is usually useful, but sometimes we need several parts of an application to work with one shared object. The Singleton Pattern provides a controlled way to create an instance once and reuse it.

In our TypeScript cache, the private constructor() prevents ordinary external construction, the static instance property stores the shared object reference, and getInstance() creates the object on the first call and returns it on later calls. The Map stores the actual data, and the cache methods provide a clear interface for saving, retrieving, checking, and clearing values.

We also discovered an important limitation: a generic class and a static singleton do not automatically provide one safe instance per generic type. Our implementation uses any, which means it should only be used under a consistent value-type assumption. Making a string cache explicitly string-based removes that particular ambiguity.

Finally, remember that a singleton is shared only within the relevant runtime and class or module context. It does not automatically share state across multiple Node.js processes, and it can make testing and dependency management more complicated if used carelessly.

The most important takeaway is this: use the Singleton Pattern when you have a clear reason to control instance creation and share state. Understand the scope of that shared state, keep the type contract honest, and choose the simplest design that solves your actual problem.

Further Reading

For a deeper understanding of the concepts used in this tutorial, explore the official TypeScript documentation:

  • TypeScript Handbook: Classes
  • TypeScript Handbook: Generics
  • TypeScript Handbook: Interfaces
  • Node.js Documentation: Modules

Support

Buy author a coffee

Support
SummarizeSendShareTweetScan
Previous Post

Dynamic Array Implementation Modern C++

Next Post

The 5 Best Books for Learning Machine Learning Mathematics

Amritesh Kumar

Amritesh Kumar

My goal here is to simplify AI for all. Please help me improve this platform by contributing your knowledge on machine learning, web dev and data science, or help me improve current tutorials. I want to keep all the resources free except for support and notes.

Related Posts

The Node.js Process Object and Environment Configuration Explained

Node.js EventEmitter Explained: Events, Listeners, and Event-Driven Architecture

DNS And TCP in Node.js Explained

HTTPS and TLS in Node.js Explained : Encryption, Certificates, Handshakes, and Secure Servers

HTTP Explained: History, Protocol Internals, and Building a Web Server with Node.js

Node.js Streams Explained: Readable, Writable, Transform, and Backpressure

Next Post
Close-up of a smartphone displaying ChatGPT app held over AI textbook.

The 5 Best Books for Learning Machine Learning Mathematics

Nano Banana 2: Google’s New AI Image Generator Just Fixed the Speed vs. Quality Problem

Nano Banana 2: Google’s New AI Image Generator Just Fixed the Speed vs. Quality Problem

Inside OpenAI Military Deal and the Ousting of Anthropic

Inside OpenAI Military Deal and the Ousting of Anthropic

Support

Buy author a coffee

Support

Support Neuraldemy

Newsletter

Top rated products

  • Probability and Statistics for Machine Learning and Data Science Probability and Statistics for Machine Learning and Data Science
    Rated 5.00 out of 5
    30.00$ Original price was: 30.00$.12.99$Current price is: 12.99$.
  • SVM Notes: Optimization & Implementation SVM Notes: Optimization & Implementation
    Rated 5.00 out of 5
    9.99$ Original price was: 9.99$.4.99$Current price is: 4.99$.
  • spidy mail hunter software Spidy Mail Hunter: Powerful And Intelligent Website & PDF Email Extractor Software (Windows App)
    Rated 4.78 out of 5
    149.00$ Original price was: 149.00$.49.00$Current price is: 49.00$.
  • Linear Algebra For Machine Learning And Data Science Linear Algebra For Machine Learning And Data Science
    Rated 4.71 out of 5
    40.00$ Original price was: 40.00$.20.99$Current price is: 20.99$.
  • Starter Website Package Infographic Starter Business & Creator Website Package (3 Pages + 1 Year Free Hosting) 199.00$ Original price was: 199.00$.149.00$Current price is: 149.00$.

Recent Posts

The Node.js Process Object and Environment Configuration Explained

Node.js EventEmitter Explained: Events, Listeners, and Event-Driven Architecture

DNS And TCP in Node.js Explained

November 2026
M T W T F S S
 1
2345678
9101112131415
16171819202122
23242526272829
30  
« Oct    

Neuraldemy

Neuraldemy

Neuraldemy

Software Development

Neuraldemy helps you learn ML, AI, Web Dev and data science from scratch. Addtionally, we also provide web development and hosting services for businesses, and individuals

[convertkit form=7049059]
  • Get Started
  • Contact
  • Book Services
  • Privacy Policy
  • Terms Of Use
  • Support Portal
  • Managed Hosting Terms
  • Web Development Terms
  • Refund Policy
Neuraldemy

© 2026 - A learning platform by Odist Magazine

Welcome Back!

Login to your account below

Forgotten Password? Sign Up

Create New Account!

Fill the forms below to register

*By registering into our website, you agree to the Terms & Conditions and Privacy Policy.
All fields are required. Log In

Retrieve your password

Please enter your username or email address to reset your password.

Log In
No Result
View All Result
Join Our WhatsApp Channel
  • Home
  • Tutorials
    • Machine Learning
    • Python
    • Web Development
    • Javascript
    • Mathematics
    • Deep Learning
    • Artificial Intelligence
  • Store
  • Our Services
    • Managed Hosting Services
    • Web Development Services
    • Digital Marketing Services
    • Website Security Checker
    • Video Editing Services
  • Our Softwares
    • JaggoRe AI
    • Email Extractor
    • TimeWell App
  • Login
  • Sign Up
  • Cart
Order Details

© 2026 - A learning platform by Odist Magazine

This website uses cookies. By continuing to use this website you are giving consent to cookies being used.
0