Skip to content

Singleton - A Design Pattern for Single Instance Management

The Singleton is a creational design pattern that ensures a class has only one instance and provides a global point of access to it. It is widely used for managing shared resources like configurations, database connections, or logging services.


The Singleton pattern restricts the instantiation of a class to a single instance and provides a global point of access to it. This is useful when exactly one object is needed to coordinate actions across the system.


Configuration Management

A single source of truth for application settings.

Database Connection

A single connection instance to prevent resource leaks.

Shared Cache

Avoid data duplication in memory.

Centralized Logging

A single logger for the entire application.

Advantages ✅Disadvantages ❌
Strict control over instantiation.Hard to test (shared global state).
Global access to the unique instance.Can introduce hidden dependencies.
Resource efficiency (e.g., a single database connection).Thread safety issues if poorly implemented.

A non-thread-safe Singleton can create multiple instances if two threads instantiate it simultaneously. While JavaScript/TypeScript is single-threaded, understanding best practices is crucial for asynchronous environments.

class Singleton {
private static instance: Singleton | null = null;
private constructor() {}
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}

Simple and effective in single-threaded environments.


class Database {
private static instance: Database;
private constructor() {
console.log("Connecting to the database...");
}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
public query(sql: string): void {
console.log(`Executing: ${sql}`);
}
}
// Usage
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true (same instance)

store.js
let instance = null;
class Store {
constructor() {
if (!instance) {
this.state = { count: 0 };
instance = this;
}
return instance;
}
increment() {
this.state.count++;
}
}
export const store = new Store();
<script setup>
import { store } from './store';
store.increment();
console.log(store.state.count); // 1
</script>

  • Stateful Services: If two parts of the application simultaneously modify shared state, it can lead to unexpected behavior.
  • Unit Testing: Singletons make testing more difficult (global state).
  • Scalability: In a multi-instance environment, a Singleton is no longer “global” to the entire application.

  1. Identify the Need Determine if your application truly needs a single instance for a specific resource or service.
  2. Choose the Right Approach Decide between lazy initialization, eager initialization, or double-checked locking based on your environment.
  3. Implement the Singleton Write the class with a private constructor and a static method to access the instance.
  4. Test Thoroughly Ensure thread safety and proper instantiation in your environment.
  5. Document the Usage Clearly document the Singleton’s purpose and how to access its instance.

Use Lazy Initialization

Lazy initialization is simple and effective for most JavaScript/TypeScript applications.

Avoid Global State

Minimize the use of global state to make your application more testable.

Document the Singleton

Clearly document the Singleton’s purpose and usage to avoid misuse.

Consider Alternatives

In many cases, a simple object literal or module can replace a Singleton.



Let’s build a simple logging service using the Singleton pattern:

class Logger {
private static instance: Logger;
private logs: string[] = [];
private constructor() {}
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public log(message: string): void {
this.logs.push(message);
console.log(message);
}
public getLogs(): string[] {
return this.logs;
}
}
// Usage
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
logger1.log("Application started.");
logger2.log("User logged in.");
console.log(logger1.getLogs()); // ["Application started.", "User logged in."]

  • Consistency: Ensures a single point of control for shared resources.
  • Resource Efficiency: Avoids unnecessary duplication of resources.
  • Global Access: Provides a well-known access point to the instance.