How to implement singleton pattern in JavaScript

The singleton pattern ensures a class has only one instance and provides a global access point to it, useful for managing shared resources like database connections, caches, or configuration. As the creator of CoreUI with 26 years of JavaScript development experience, I’ve implemented singletons in large-scale applications to manage global state, coordinate logging systems, and ensure single database connection pools across millions of requests.

The most maintainable approach uses ES6 modules for natural singleton behavior or static properties for class-based singletons.

ES6 Module Singleton

// logger.js
class Logger {
  constructor() {
    this.logs = []
  }

  log(message) {
    this.logs.push({ message, timestamp: Date.now() })
    console.log(`[${new Date().toISOString()}] ${message}`)
  }

  getLogs() {
    return this.logs
  }
}

// Export single instance
export default new Logger()

// Usage in other files
// app.js
import logger from './logger.js'

logger.log('Application started')
logger.log('User logged in')

// admin.js
import logger from './logger.js'

console.log(logger.getLogs())
// Same instance, sees all logs from app.js

Class-Based Singleton

class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance
    }

    this.connection = null
    Database.instance = this
  }

  connect(connectionString) {
    if (!this.connection) {
      this.connection = connectionString
      console.log('Connected to:', connectionString)
    }
  }

  getConnection() {
    return this.connection
  }
}

// Usage
const db1 = new Database()
db1.connect('mongodb://localhost:27017')

const db2 = new Database()
console.log(db1 === db2) // true (same instance)
console.log(db2.getConnection()) // 'mongodb://localhost:27017'

Lazy Initialization Singleton

class ConfigManager {
  static instance = null

  constructor() {
    if (ConfigManager.instance) {
      return ConfigManager.instance
    }

    this.config = {}
    ConfigManager.instance = this
  }

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

    return ConfigManager.instance
  }

  set(key, value) {
    this.config[key] = value
  }

  get(key) {
    return this.config[key]
  }

  getAll() {
    return { ...this.config }
  }
}

// Usage
const config1 = ConfigManager.getInstance()
config1.set('apiUrl', 'https://api.example.com')

const config2 = ConfigManager.getInstance()
console.log(config2.get('apiUrl')) // 'https://api.example.com'
console.log(config1 === config2) // true

Singleton with Private Constructor

class Cache {
  static #instance = null

  #cache = new Map()

  constructor(token) {
    if (token !== Cache.#getToken()) {
      throw new Error('Use Cache.getInstance() to get instance')
    }
  }

  static #getToken() {
    return Symbol('CacheToken')
  }

  static getInstance() {
    if (!Cache.#instance) {
      Cache.#instance = new Cache(Cache.#getToken())
    }

    return Cache.#instance
  }

  set(key, value) {
    this.#cache.set(key, value)
  }

  get(key) {
    return this.#cache.get(key)
  }

  has(key) {
    return this.#cache.has(key)
  }

  clear() {
    this.#cache.clear()
  }
}

// Usage
// const cache = new Cache() // Error: Use getInstance()

const cache1 = Cache.getInstance()
cache1.set('user', { name: 'John' })

const cache2 = Cache.getInstance()
console.log(cache2.get('user')) // { name: 'John' }

Singleton with Initialization Options

class ApiClient {
  static instance = null

  constructor(config) {
    if (ApiClient.instance) {
      return ApiClient.instance
    }

    this.baseUrl = config.baseUrl
    this.apiKey = config.apiKey
    this.timeout = config.timeout || 5000

    ApiClient.instance = this
  }

  static getInstance(config) {
    if (!ApiClient.instance) {
      if (!config) {
        throw new Error('ApiClient requires configuration on first call')
      }
      ApiClient.instance = new ApiClient(config)
    }

    return ApiClient.instance
  }

  async get(endpoint) {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      headers: { 'X-API-Key': this.apiKey }
    })
    return response.json()
  }
}

// Usage
// First call requires config
const api1 = ApiClient.getInstance({
  baseUrl: 'https://api.example.com',
  apiKey: 'secret-key'
})

// Subsequent calls don't need config
const api2 = ApiClient.getInstance()
console.log(api1 === api2) // true

await api2.get('/users')

Thread-Safe Singleton (Node.js)

const { Worker } = require('worker_threads')

class ConnectionPool {
  static instance = null
  static lock = false

  constructor() {
    if (ConnectionPool.instance) {
      return ConnectionPool.instance
    }

    this.connections = []
    this.maxConnections = 10
    ConnectionPool.instance = this
  }

  static async getInstance() {
    // Wait for lock
    while (ConnectionPool.lock) {
      await new Promise(resolve => setTimeout(resolve, 10))
    }

    ConnectionPool.lock = true

    try {
      if (!ConnectionPool.instance) {
        ConnectionPool.instance = new ConnectionPool()
        await ConnectionPool.instance.initialize()
      }

      return ConnectionPool.instance
    } finally {
      ConnectionPool.lock = false
    }
  }

  async initialize() {
    console.log('Initializing connection pool...')
    // Async initialization logic
    await new Promise(resolve => setTimeout(resolve, 100))
  }

  getConnection() {
    // Return connection from pool
    return this.connections[0] || 'new-connection'
  }
}

// Usage
const pool1 = await ConnectionPool.getInstance()
const pool2 = await ConnectionPool.getInstance()
console.log(pool1 === pool2) // true

Resettable Singleton (for testing)

class Analytics {
  static instance = null

  constructor() {
    if (Analytics.instance) {
      return Analytics.instance
    }

    this.events = []
    Analytics.instance = this
  }

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

    return Analytics.instance
  }

  static reset() {
    Analytics.instance = null
  }

  track(event) {
    this.events.push({ event, timestamp: Date.now() })
  }

  getEvents() {
    return this.events
  }
}

// Usage in tests
describe('Analytics', () => {
  afterEach(() => {
    Analytics.reset() // Reset singleton between tests
  })

  it('tracks events', () => {
    const analytics = Analytics.getInstance()
    analytics.track('page_view')
    expect(analytics.getEvents().length).toBe(1)
  })

  it('starts fresh', () => {
    const analytics = Analytics.getInstance()
    expect(analytics.getEvents().length).toBe(0)
  })
})

Singleton with Dependency Injection

class ServiceContainer {
  static instance = null

  constructor() {
    if (ServiceContainer.instance) {
      return ServiceContainer.instance
    }

    this.services = new Map()
    ServiceContainer.instance = this
  }

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

    return ServiceContainer.instance
  }

  register(name, service) {
    this.services.set(name, service)
  }

  get(name) {
    if (!this.services.has(name)) {
      throw new Error(`Service ${name} not registered`)
    }

    return this.services.get(name)
  }
}

// Usage
const container = ServiceContainer.getInstance()

// Register services
container.register('logger', new Logger())
container.register('cache', new Cache())

// Use services anywhere
const logger = container.get('logger')
logger.log('Application started')

const cache = container.get('cache')
cache.set('key', 'value')

Best Practice Note

This is how we implement singletons in CoreUI for managing shared resources like API clients and configuration. The singleton pattern provides controlled access to shared state, but use it sparingly as it can make testing difficult and create hidden dependencies. Prefer ES6 modules for natural singleton behavior, make singletons resettable for testing, and consider dependency injection containers for managing multiple singleton services.

For related patterns, check out how to implement factory pattern in JavaScript and how to implement module pattern in JavaScript.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
What is the difference between typeof and instanceof in JavaScript
What is the difference between typeof and instanceof in JavaScript

How to Remove Underline from Link in CSS
How to Remove Underline from Link in CSS

How to loop through a 2D array in JavaScript
How to loop through a 2D array in JavaScript

What is Double Question Mark in JavaScript?
What is Double Question Mark in JavaScript?

Answers by CoreUI Core Team