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
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
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");
}
}
There are three important pieces here.
1. A static field stores the instance
static instance;
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();
}
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();
code that follows this pattern does this:
const popup = Popup.getInstance();
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
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
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();
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");
}
}
Now even direct construction returns the same object:
const first = new Popup();
const second = new Popup();
console.log(first === second); // true
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>
The page creates access to the manager once:
const windowManager = WindowManager.getInstance();
Other parts of the application can call getInstance() again without creating another manager.
const anotherReference = WindowManager.getInstance();
console.log(
windowManager === anotherReference
); // true
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;
You can import it elsewhere:
import appStore from "./appStore.js";
appStore.setUser({
id: 42,
name: "Alex"
});
And another module can import the same object:
import appStore from "./appStore.js";
console.log(appStore.user);
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;
}
That's lazy initialization.
You can also create the instance immediately:
class Config {
static instance = new Config();
}
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;
}
}
Two consumers share the same state:
const first = Counter.getInstance();
const second = Counter.getInstance();
first.increment();
console.log(second.count); // 1
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
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;
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);
}
with explicit dependency injection:
function savePost(post, database) {
database.save(post);
}
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;
you could simply export the service:
// logger.js
export function log(message) {
console.log(message);
}
Or export one configured object:
// logger.js
class Logger {
log(message) {
console.log(message);
}
}
export const logger = new Logger();
Then use it wherever it is needed:
import { logger } from "./logger.js";
logger.log("Application started");
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();
}
}
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.