Singleton Pattern
Singleton guarantees exactly one instance of a class per process and exposes a global access point.
Introduction
Singleton guarantees exactly one instance of a class per process and exposes a global access point. In production you'll see it for configuration registries, logging facades, connection pools, metrics collectors, and cached SDK clients — anywhere a stateful resource is genuinely process-wide.
It is also the most misused pattern in the catalog: static accessors hide dependencies, poison tests, and mask circular init order. This lesson covers classic implementations (eager, lazy, Bill Pugh holder, enum), threading hazards, and the modern replacement — DI-scoped singleton beans.
The story
A trading platform hit a 4 a.m. incident where its custom Logger singleton silently dropped messages under load. Double-checked locking ran without volatile; two threads built two loggers and one was GC'd mid-write. The fix wasn't smarter locking — it was a DI-scoped Logger so tests could observe the bug deterministically.
The business problem
Hand-rolled Singletons create operational and engineering debt that compounds with team size:
- Hidden dependencies:
getInstance()deep in methods — callers don't declare what they use. - Test pollution: shared state leaks between cases; suites pass locally, fail in CI.
- Init-order bugs: singletons calling singletons surface circular deps at first runtime call.
- Concurrency traps: naive lazy init is not thread-safe in Java; DCL needs
volatile.
The problem teams faced
Before applying Singleton (or its DI equivalent), teams hit these pains:
- Multiple instances of a shared resource (cache, pool, registry) cause inconsistency.
- Passing the same object through every constructor is noisy — but static access is worse.
- Configuration must initialize once at startup and never twice.
- Subsystems instantiate duplicates instead of reusing a controlled instance.
Understanding the topic
Intent: Ensure a class has exactly one instance and provide a global access point.
- Singleton class — private constructor, static
instance, static accessor. - Client — calls
getInstance()instead ofnew. - DI-scoped bean (preferred) — container enforces one instance; clients receive an interface via constructor.
Internal architecture
Classic vs DI-scoped structure:
// Classic (textbook)Singleton.getInstance().log(msg)// Modern (preferred)class OrderService {constructor(private logger: Logger) {} // container wires one Logger}// Composition rootcontainer.register(Logger, { scope: "singleton" })
Visual explanation
Three diagrams cover Singleton mechanics, hazards, and the modern DI path:
Informative example
Before / after — static Singleton vs DI-scoped singleton:
// ❌ Hidden dependency — untestable without PowerMockclass OrderService {placeOrder(cart: Cart) {Logger.getInstance().info("Placing order");PaymentGateway.getInstance().charge(cart.total);}}// ✅ Same uniqueness — explicit dependenciesinterface Logger { info(msg: string): void }interface PaymentGateway { charge(amount: Money): Promise<void> }class OrderService {constructor(private logger: Logger,private payments: PaymentGateway,) {}async placeOrder(cart: Cart) {this.logger.info("Placing order");await this.payments.charge(cart.total);}}// Composition root — one instance each in productionconst app = createApp({logger: new WinstonLogger(),payments: new StripeGateway(process.env.STRIPE_KEY),});
Execution workflow
First access
Client invokes getInstance() or container resolves bean.
Real-world use
Java Runtime.getRuntime(), Spring default @Component scope, Logback LoggerContext, AWS SDK v3 clients cached at module level, React QueryClient, Prometheus registries. Each is "one per process" — but production code exposes them through DI, not static accessors.
Production case study
Stripe client singleton migration:
- Scenario: SaaS payments used
StripeClient.getInstance(). - Symptom: Integration tests left charges from prior suites — singleton held authenticated session.
- Decision: One client in production via DI container; per-test
FakeStripeClient. - Outcome: Flaky payment tests dropped to zero; production behaviour unchanged.
- Lesson: "One instance per process" ≠ "static accessor in every caller."
Trade-offs
- Pro: guaranteed unique instance for shared resources.
- Pro: lazy initialization with proper variant (holder, enum).
- Con: hidden dependencies — refactors miss call sites.
- Con: test pollution unless reset hooks or per-test containers exist.
- Con: concurrency hazards with naive lazy init in multi-threaded runtimes.
Decision framework
- Use when the resource is genuinely process-wide (pool, metrics registry, config snapshot).
- Use when a DI container enforces uniqueness and injects an interface.
- Avoid when passing the object feels tedious — that's a wiring problem, not a Singleton problem.
- Avoid for per-request, per-tenant, or per-user state.
Best practices
- Java: Bill Pugh holder or enum singleton (Joshua Bloch) — serialization and reflection safe.
- TypeScript: module-scoped instance — Node caches modules; no class ceremony needed.
- Always inject through constructors in app code; let the container own "oneness."
- Provide reset hooks or test containers for integration tests.
Anti-patterns to avoid
- UserContext singleton storing current user — per-request data on process-wide state.
- Singleton calling Singleton — circular deps invisible until runtime.
- Global variables dressed as "pragmatic" Singleton classes.
Common mistakes
- Double-checked locking without volatile in Java — two instances possible.
- Serialization creating a second instance — use enum or readResolve.
- Forgetting test reset — order-dependent flaky suites.
Debugging tips
- Tests pass individually but fail in suite → suspect shared singleton state.
- Log getInstance() call count in staging — should be 1 per process lifetime.
- Diff DI wiring between environments when behaviour diverges.
Optimization strategies
- Eager init at startup for latency-critical paths (avoid first-request penalty).
- Lazy init for expensive resources not needed on every deployment profile.
Common misconceptions
- Singleton ≠ DI. DI singleton scope gives uniqueness without static access.
- Related: Factory Method — containers use factory methods internally.
- Related: Service Locator — generalizes lookup; same hidden-dependency problem.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionWhy is naive lazy Singleton not thread-safe in Java?+
Answer
Follow-up
2AdvancedQuestionWhen pick enum Singleton over Bill Pugh holder?+
Answer
Follow-up
3BeginnerQuestionHow is Spring @Component different from hand-coded Singleton?+
Answer
Follow-up
4IntermediateQuestionWhat's wrong with Singleton for current user?+
Answer
Follow-up
Summary
You can explain Singleton mechanics, threading hazards, and why DI-scoped beans replace static accessors. Tell the Logger incident story and the Stripe test-flake case — if you can do that, you own Singleton.