← Back to principles

Design Principle

Singleton Pattern: Principles, Trade-offs & Examples

Learn the Singleton pattern: ensure a class has only one instance with global access. Understand when to use it, common pitfalls, thread safety, and better alternatives like dependency injection.

Overview: Singleton Pattern: Principles, Trade-offs & Examples

Singleton Pattern

Why Engineers Care About This

Think of the Singleton pattern like one main control panel for a building. You do not want multiple panels fighting over the same switches—that creates confusion and conflicts. Singleton enforces one instance of a class and a single global access point (getInstance() or a module export).

That matters when a resource should not be duplicated accidentally: a connection pool inside one process, a configuration loader, or a metrics registry. Singleton is a creational guardrail, not a license to make everything global.

In interviews, when someone asks "How would you ensure only one instance of a class exists?", they are testing whether you know when Singleton fits, how to make it thread-safe, and what breaks when you treat it as a universal hammer. Most engineers reach for Singleton for every shared resource and wonder why tests are slow and production has subtle races.

For connection pool sizing and exhaustion under load, see Database Connection Pooling.

Core Intuitions You Must Build

  • Singleton guarantees one instance per classloader (or module), not one instance for your entire system. Twelve pods means twelve Singleton instances unless you move shared state to Redis, the database, or a dedicated service.

  • Global access is convenient; global state is expensive. Anything reachable via getInstance() from anywhere is hard to reason about, hard to reset between tests, and easy to misuse as a hidden dependency.

  • Lazy initialization and thread safety are separate problems. A naive lazy Singleton is not thread-safe. Eager init, double-checked locking, or the holder idiom solve races—but each has trade-offs.

  • "One pool" is not the same as "Singleton class." You usually want one connection pool per process, injected where needed—not a static DatabaseConnection.getInstance() that every layer calls directly.

  • Dependency injection is often the better single-instance story. The container (or main function) creates one instance and passes it in. You still have one object; dependencies are explicit and mockable.

  • Loggers and config are the classic yes; business services are usually no. Logging adapters and read-mostly config loaders tolerate global access. Payment handlers, cart state, and tenant-specific caches do not.

  • Alternatives exist for every creational excuse. Factory with registry, module-level export const, framework-managed beans, and external stores each replace Singleton in specific contexts—know which lever to pull.

Implementation approaches compared

Four common ways to implement Singleton differ in thread safety, lazy vs eager init, and complexity:

Comparison of eager init, lazy check, double-check locking, and holder/module Singleton implementations

ApproachThread-safe?Lazy?ComplexityPick when
Eager static fieldYesNo (created at class load)LowestSmall, always-needed objects; startup cost is fine
Lazy getInstance() (naive)NoYesLowSingle-threaded code only—not production Java/Go servers
Double-checked lockingYes (with volatile/sync)YesMediumExpensive lazy init in multi-threaded runtimes; know memory visibility
Holder / module exportYesYesLow–mediumDefault recommendation in modern Java (holder) or Node (module cache)
Client code
getInstance() ──first call?──► create instance (sync if needed)
    │                              │
    └──────── same reference ◄─────┘

Where the instance actually lives

Before you implement, answer: one instance per what?

Singleton scope across single process, multiple workers, and a clustered fleet

Per-process Singleton is correct for an in-memory pool. Per-cluster "Singleton" requires an external system of record—not a static field on every pod.

Subtopics (Taught Through Real Scenarios)

Singleton vs Dependency Injection

What people usually get wrong:

Engineers treat Singleton and "single instance" as the same thing. They are not. Singleton is a pattern for global access. Dependency injection is a pattern for explicit dependencies. You can have exactly one Logger in your app without any class exposing getInstance()—you construct it once in main or your DI container and pass it in.

How this breaks systems in the real world:

A payment service called ConfigManager.getInstance() from forty files. Onboarding a new engineer meant tracing static calls across packages. Unit tests mutated global config between cases and flaked in CI. The team introduced constructor injection: one Config instance created at startup, passed into services. Behavior stayed "single instance," but dependencies became visible and mockable. The fix was not deleting the single object—it was stopping global reachability.

What interviewers are really listening for:

Junior engineers say "use Singleton for shared resources." Senior engineers say "I need one instance—Singleton is one way; DI with a single wired instance is usually better for testability." They want you to name hidden dependencies and interface boundaries, not recite private constructor.

Thread-Safe Initialization Under Concurrency

What people usually get wrong:

Teams copy a lazy if (!instance) instance = new Singleton() snippet and assume the JVM or runtime "handles it." Under concurrency, two threads can pass the first check and create two instances—or worse, publish a half-initialized object without proper visibility.

How this breaks systems in the real world:

A metrics registry used lazy Singleton init on first request. During a traffic spike after deploy, two worker threads initialized separate registries. Dashboards showed double-counted counters on one host while another showed gaps. The fix was the initialization-on-demand holder idiom in Java (classloader guarantees) or a module-level singleton in Node where the module cache is the lock. But the real lesson is: first-request races are real in servlet containers and async servers—not textbook trivia.

What interviewers are really listening for:

They want eager vs lazy vs double-check vs holder—and why volatile matters in Java double-checked locking. Junior engineers describe one getInstance(). Senior engineers name the memory model issue and prefer holder/module idiom unless there is a strong reason not to.

Singleton Across Processes and Pods

What people usually get wrong:

"If it's a Singleton, the whole system shares one." False. Each process (and each pod) loads its own class or module. A file-writing logger Singleton in four Gunicorn workers means four instances hammering one log file.

How this breaks systems in the real world:

A batch worker used a Singleton logger writing to /var/log/app.log. With four processes, interleaved writes corrupted JSON log lines. Centralized logging (stdout → collector) replaced file Singletons. For cluster-wide rate limits or locks, the team used Redis—not a static field. But the real lesson is: scope the word "single" before you design.

What interviewers are really listening for:

You distinguish process-local Singleton from distributed coordination. Junior engineers draw one box. Senior engineers say "one per JVM, N on the fleet—external store or service for shared state."

Connection Pools, Loggers, and Config Loaders

What people usually get wrong:

"Database connections should be Singleton." Closer to the truth: one pool per process, sized for that process's concurrency—not a god object every repository reaches into via static access. Pools still need max size, timeout, and health checks.

How this breaks systems in the real world:

A service wrapped a 20-connection pool in DatabaseConnection.getInstance(). Traffic doubled; wait times spiked because the pool max was hard-coded inside the Singleton constructor. Scaling horizontally added more pods (good) but each pod still capped at 20 (bad per pod, okay fleet-wide only if intentional). The fix: inject a Pool with config from env, tune max_connections, and monitor wait time. Singleton was not the problem—unchangeable global construction was.

What interviewers are really listening for:

They want pool per process, injection, and exhaustion behavior—not "Singleton = one connection." Link to operational symptoms: timeouts, pool wait, too many connections on the database.

Testing, Hidden Dependencies, and Global State

What people usually get wrong:

Singletons feel harmless until the test suite calls real APIs, writes real files, or depends on order because getInstance() caches state from the previous test.

How this breaks systems in the real world:

A team had FeatureFlags.getInstance() read from disk on first access. Tests toggled flags by mutating files; parallel CI workers saw random failures. Refactor: FeatureFlags interface + in-memory fake in tests; production wiring creates one real instance at bootstrap. Same runtime shape, no static coupling.

When Singletons block testing, the failure modes cluster around mocking, order, and hidden calls:

Singleton failure modes: flaky tests, race at init, and pool exhaustion

What interviewers are really listening for:

You propose interfaces, seams, and reset hooks—or DI—not "we don't unit test that layer." Senior signal: "I'd still have one instance in production; tests get a fake via constructor."


Implementation

Basic Singleton (not thread-safe)

Use only in single-threaded contexts. In servers, this is a bug waiting for first spike traffic.

TypeScript
Python
Java
1

Java — initialization-on-demand holder (lazy, thread-safe, no explicit locking):

public class ConfigLoader {
    private ConfigLoader() { /* load config */ }

    private static class Holder {
        static final ConfigLoader INSTANCE = new ConfigLoader();
    }

    public static ConfigLoader getInstance() {
        return Holder.INSTANCE;
    }
}

TypeScript / Node — module cache (idiomatic single instance):

// logger.ts — importers share one instance
class Logger {
  log(message: string) {
    console.log(message);
  }
}

export const logger = new Logger();

Double-checked locking (when you must lazy-init with sync)

In Java, instance must be volatile so other threads see a fully constructed object:

Java
Python
1

Example: injectable pool (preferred over static database Singleton)

// Composition root — one pool per process
const pool = createPool({ max: 20, connectionString: process.env.DATABASE_URL });

export function buildUserRepository() {
  return new UserRepository(pool);
}

class UserRepository {
  constructor(private readonly pool: Pool) {}

  async findById(id: string) {
    return this.pool.query('SELECT * FROM users WHERE id = $1', [id]);
  }
}

Tests pass a small fake pool; production passes the real one—no getInstance() in domain code.


Interview questions to practice

  • You need one config object in a Spring app—Singleton class or @Bean scoped singleton? What changes for unit tests?
  • Walk me through a thread-safe lazy Singleton in Java—why is volatile part of double-checked locking?
  • Four API pods each have a "Singleton" cache—do all users see the same cache entries? What would you use instead?
  • Our tests flake after adding Metrics.getInstance()—what dependency shape fixes it without changing production cardinality?
  • When does a connection pool belong in a Singleton vs injected at startup? What metric tells you the pool is wrong-sized?
  • Compare Singleton to a Factory that returns a shared instance—when is each appropriate?

FAQs

Q: What is the Singleton pattern in one sentence?

A: A creational pattern that restricts a class to one instance and exposes global access—typically via a private constructor and getInstance().

Q: Singleton vs dependency injection—which wins?

A: DI wins for testability and clarity when you still want one instance. Singleton wins only when you truly need global access with no injection point—rare in modern services. Prefer "single instance wired once" over "static getter everywhere."

Q: Is Singleton thread-safe by default?

A: No. Naive lazy initialization races under concurrency. Use eager init, double-checked locking with correct visibility, holder idiom, or language guarantees (module cache, enum in Java).

Q: Can I use Singleton for a database connection pool?

A: You want one pool per process, usually created at startup and injected—not a static god object every layer calls. Size the pool from config; monitor wait time and exhaustion. See Database Connection Pooling for operational detail.

Q: Why do interviewers dislike Singleton?

A: It encodes global state, hides dependencies, and complicates tests. Strong candidates acknowledge the trade-off and name alternatives (DI, factories, external stores) instead of defending getInstance() by reflex.

Q: How do I get one instance across a whole cluster?

A: Not with a class-level Singleton. Use Redis, your database, a dedicated coordinator service, or sticky routing with eyes open to failure modes. The pattern is process-local.


Key Takeaways

Scope "single" first — per classloader/process vs per fleet; pods multiply instances unless state lives outside the JVM

Global access ≠ good design — convenience today becomes hidden dependencies and flaky tests tomorrow

Holder/module over hand-rolled locks — default to idiomatic lazy singletons your language already provides

DI preserves cardinality — one wired instance without `getInstance()` sprawl through domain code

Pools and loggers are process resources — configure, inject, and monitor them; do not freeze limits inside static constructors

Cluster coordination needs external stores — Redis, DB, or services—not static fields on every replica

Interview signal is trade-offs — name thread safety, test seams, and when you would refuse Singleton

Keep exploring

Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.