Singleton

category Creational
popularity

The Singleton pattern ensures a class has only one instance and provides a global access point to that instance. Both parts matter: the enforcement (no new MyClass() elsewhere in the codebase), and the accessor (a getInstance() or equivalent that always returns the same object).

In practice, developers reach for the "global access" half and add the "single instance" constraint as a side effect. That conflation causes most of the problems associated with the pattern.

When it makes sense

Some things genuinely should exist exactly once per process: a configuration object loaded from environment variables at startup, a database connection pool shared across the application, a logger writing to a single file descriptor. Singleton semantics are appropriate here because multiple instances would either waste resources (pools are expensive to initialize) or produce incorrect behavior (two loggers writing interleaved lines to the same file).

The key phrase is "per process." A connection pool singleton in an application is fine. A connection pool singleton inside a library that other applications will import is a different situation — you've made a lifecycle decision on behalf of the library's callers.

Implementation

Go

Go has no classes, which strips the pattern down to its core. A package-level variable initialized with sync.Once is the idiomatic approach:

package config

import (
	"os"
	"sync"
)

type Config struct {
	DatabaseURL string
	Port        string
}

var (
	instance *Config
	once     sync.Once
)

func Get() *Config {
	once.Do(func() {
		instance = &Config{
			DatabaseURL: os.Getenv("DATABASE_URL"),
			Port:        envOr("PORT", "8080"),
		}
	})
	return instance
}

func envOr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

sync.Once is goroutine-safe. The initializer runs exactly once regardless of how many goroutines call Get() concurrently — no double-checked locking, no mutex to manage by hand.

For cases where initialization has no runtime dependencies and can't fail, a package-level var is simpler and equally safe:

var DefaultLogger = &Logger{output: os.Stdout, level: INFO}

Go's package initialization runs before main(), so this is safe as long as you're not dealing with circular package dependencies. If the initialization order between packages matters, prefer sync.Once.

TypeScript

ES modules are cached after first evaluation. Every import of the same module path gets the same binding, which makes a module-level export the simplest singleton implementation:

// config.ts
const config = {
  databaseUrl: process.env.DATABASE_URL ?? '',
  port: parseInt(process.env.PORT ?? '8080', 10),
  environment: (process.env.NODE_ENV ?? 'development') as 'development' | 'production' | 'test',
} as const;

export default config;

Anywhere in the codebase: import config from './config'. The module evaluates once; every importer gets the same object.

When initialization is expensive and should be deferred until first use:

// pool.ts
import { Pool } from 'pg';

let pool: Pool | null = null;

export function getPool(): Pool {
  if (!pool) {
    pool = new Pool({ connectionString: process.env.DATABASE_URL });
  }
  return pool;
}

This is single-threaded (Node.js doesn't run JavaScript concurrently), so there's no race condition on the null check. If you're using worker threads and genuinely need synchronization, that's a different problem — but it's rare enough in Node.js that the simple null check is correct for the vast majority of cases.

The class-based version you'll find in many TypeScript tutorials — private static instance: Singleton, private constructor(), static getInstance() — is Java idiom transplanted verbatim into TypeScript. The language doesn't need it.

Python

Python's import system caches modules in sys.modules after the first import. Re-importing a module returns the cached version, which makes the module-level instance pattern work the same way as Go's package-level approach:

# config.py
import os


class _Config:
    def __init__(self) -> None:
        self.database_url: str = os.environ.get("DATABASE_URL", "")
        self.port: int = int(os.environ.get("PORT", "8080"))
        self.debug: bool = os.environ.get("DEBUG", "").lower() in ("1", "true", "yes")


config = _Config()
from config import config

print(config.database_url)

The underscore prefix on _Config signals that the class is an implementation detail — only config is part of the module's public interface. This is a convention rather than enforcement, but it's useful: anyone reading the code knows not to instantiate _Config directly.

For thread-safe lazy initialization — relevant when using threading but less critical in async frameworks that run on a single event loop:

import threading
from typing import Optional


_lock = threading.Lock()
_instance: Optional["ExpensiveResource"] = None


def get_resource() -> "ExpensiveResource":
    global _instance
    if _instance is None:
        with _lock:
            if _instance is None:
                _instance = ExpensiveResource()
    return _instance

The double-checked locking here — checking _instance before acquiring the lock, then checking again inside — avoids locking on every call once the instance exists. In CPython, the GIL makes the outer check safe, but the pattern is written to be correct regardless of GIL behavior.

What goes wrong

The Singleton's poor reputation is mostly earned by its mutable variant. The issues:

Global mutable state. Any code anywhere in the codebase can call Config.get() and get the same object. If that object is mutable, mutations from one part of the system are visible everywhere else. This is the definition of a global variable, dressed up in a design-patterns costume.

Testing becomes order-dependent. If test A modifies the singleton and test B expects a clean default state, test order matters — and test isolation breaks. The usual fixes are adding a reset() method to the singleton (awkward) or patching at the module level (reasonable in Python, but test-only behavior that doesn't exist in production code).

Dependencies disappear from function signatures. A function that calls Logger.get() internally depends on the logger, but that dependency is invisible at the call site. Callers can't see it; test setup has to know about it implicitly. Dependency injection puts this dependency in the function's signature or constructor, where it's explicit and replaceable.

None of this applies to immutable singletons. A config object loaded once from environment variables and never written to afterward has none of these problems. The criticism is about mutable shared state, not about the "one instance" guarantee itself.

A useful question before reaching for the pattern: can the singleton's fields be const/final/readonly after initialization? If yes, it's probably fine. If callers need to write to it, stop and ask whether a different design — a dependency-injected service, a context passed through the call stack, a message bus — handles the use case without the global-state consequences.

Language fit

Language How the pattern maps
Go Package-level var or sync.Once; no class mechanics needed
TypeScript ES module cache; module-level export is the natural shape
Python sys.modules cache; module-level instance, _PrivateClass convention

All three languages make the GoF class-based Singleton largely unnecessary for application code. The pattern survives in framework and library design, where you're building abstractions over a class hierarchy and genuinely need to enforce instantiation rules across boundaries you don't control. In application code, the language-idiomatic equivalents are cleaner.