iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Singleton

The Singleton pattern restricts a class to one instance globally. Use for shared resources (config, logger, DB pool); avoid when testability and concurrency matter more.

Implementations + pitfalls + alternatives

EXAMPLE
// 1) Module-as-singleton (the JavaScript way)
// db.js
let pool = null;
export function getPool() {
    if (!pool) pool = new Pool({ /* config */ });
    return pool;
}
export function closePool() {
    pool?.end();
    pool = null;
}

// Or just
// const pool = new Pool({ /* config */ });
// export default pool;
// Modules are cached by Node — importing twice returns the same instance.

// 2) Classic singleton class
class Logger {
    static #instance = null;
    static getInstance() {
        if (!Logger.#instance) {
            Logger.#instance = new Logger();
        }
        return Logger.#instance;
    }
    constructor() {
        if (Logger.#instance) throw new Error('use Logger.getInstance()');
        // setup
    }
    info(msg) { console.log('[info]', msg); }
}

Logger.getInstance().info('hello');

// 3) Lazy + thread-safe (Java, Python — note: GIL changes things in CPython)
// Java — double-checked locking with volatile
class Config {
    private static volatile Config instance;
    private Config() {}
    public static Config getInstance() {
        if (instance == null) {
            synchronized (Config.class) {
                if (instance == null) instance = new Config();
            }
        }
        return instance;
    }
}

// Python — module-level (idiomatic)
# config.py
# _instance = None
# def get_config():
#     global _instance
#     if _instance is None:
#         _instance = Config()
#     return _instance

// 4) Enum singleton (Java's safest)
enum Logger {
    INSTANCE;
    public void info(String msg) { ... }
}
// Logger.INSTANCE.info("hi")
// Serialisation-safe + reflection-safe + lazy.

// 5) Typical real uses
//   - DB connection pool
//   - Logger
//   - App config
//   - Feature-flag client
//   - Service locator / registry (in absence of DI)
//   - Hardware access (audio context, GPU device)

// 6) Why singletons get a bad rap
//   - Implicit dependencies — hard to see who depends on what
//   - Hard to test — global state leaks between tests
//   - Hard to swap (e.g. mock in tests, switch impl in dev/prod)
//   - Don't compose well with multi-tenant or multi-config scenarios
//   - Concurrency bugs in eager initialisation
//   - Tight coupling — your code knows about the global, not a contract

// 7) Better alternative: Dependency Injection
// Define a contract; let composition root choose the instance.
//
// // logger.ts
// export interface Logger { info(msg: string): void; }
// export class ConsoleLogger implements Logger { info(msg: string) { console.log(msg); } }
//
// // service.ts
// export class UserService {
//     constructor(private logger: Logger) {}
//     create(u: User) { this.logger.info(`created ${u.id}`); }
// }
//
// // bootstrap.ts
// const logger = new ConsoleLogger();
// const svc = new UserService(logger);
//
// // test.ts
// const fake = { info: vi.fn() };
// const svc = new UserService(fake);
//
// Modern DI frameworks: Angular, NestJS, Spring, .NET DI, Dagger, Inversify.

// 8) When a singleton is the right call
//   - Truly one instance globally (process-wide cache, system clock)
//   - Lifecycle matches process lifecycle
//   - You don't need to mock it in tests (e.g. it has no behavior to mock)
//   - You can't avoid it (third-party library requirement)

// 9) Make a singleton testable
//   a. Expose a reset/init function for tests
//      export function resetForTesting() { pool = null; }
//   b. Inject a factory or pre-built instance — let production use the singleton, tests pass a mock
//   c. Use a service container — the 'singleton' is registered, not hard-coded

// 10) Concurrency pitfalls
// - Eager init in a class loader can dead-lock with other class initialisers
// - Lazy init without synchronisation can create two instances under race
// - Don't put expensive blocking work in the constructor

// 11) Memory + lifecycle
// Singletons live for the lifetime of the process. Be careful with:
//   - Listeners registered to the singleton — keep references; garbage never collected
//   - Caches that grow unbounded — set max sizes or TTL
//   - Resources (file handles, connections) — provide a clean shutdown method

// 12) Singleton vs Service Locator vs DI
// Singleton    : caller asks the class itself (Class.getInstance())
// Service Locator: caller asks a registry (Container.get('logger'))
// DI           : the caller is GIVEN its dependencies, doesn't ask
//
// DI > Service Locator > Singleton on testability + clarity.

// 13) Anti-pattern signs
//   • You sprinkle Logger.getInstance() across 200 files
//   • Tests fail because some other test left the singleton in a weird state
//   • You wish there were two instances (dev + prod, two tenants) but the singleton blocks you
//   • Adding a constructor argument requires touching every caller

// 14) Modern alternative — module + factory
// connection.ts
let client = null;
export function init(config) {
    if (client) throw new Error('already initialised');
    client = createClient(config);
    return client;
}
export function getClient() {
    if (!client) throw new Error('call init() first');
    return client;
}
export function close() {
    client?.close();
    client = null;
}
// Forces explicit init at app start; safer than implicit lazy creation.

// 15) When debating 'should this be a singleton?'
//   - Is there exactly one of this thing per process?
//   - Will tests ever need a different impl?
//   - Will multiple modules need it?
//   If yes-no-yes → DI it.
//   If yes-no-no  → module-level instance.
//   If yes-yes-yes → DI it; the singleton-ness is enforced at the composition root.

Why it matters

Singletons feel handy but punish tests + concurrency. Reach for module-level instances + dependency injection — you get the “one instance” benefit without the implicit-global headaches.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
class Config {
    static #instance;
    static get() { return this.#instance ??= new Config(); }
}
// Use sparingly — singletons make tests harder.
Try it Yourself »

Exercise

Singletons hide constructors and expose…

Config. ()

Test yourself

Q1. Singleton guarantees…
Q2. A common downside is…
Q3. A modern alternative is…

Discussion

Loading…