Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 16

    Singleton Pattern

    Singleton guarantees exactly one instance of a class per process and exposes a global access point.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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 of new.
    • DI-scoped bean (preferred) — container enforces one instance; clients receive an interface via constructor.

    Internal architecture

    Classic vs DI-scoped structure:

    ts
    // Classic (textbook)
    Singleton.getInstance().log(msg)
    // Modern (preferred)
    class OrderService {
    constructor(private logger: Logger) {} // container wires one Logger
    }
    // Composition root
    container.register(Logger, { scope: "singleton" })

    Visual explanation

    Three diagrams cover Singleton mechanics, hazards, and the modern DI path:

    Singleton lifecycle
    First getInstance()
    instance == null
    Create once
    Holder / enum / sync
    Cache reference
    Process lifetime
    Return cached
    Hot path — no lock
    Bill Pugh holder and enum avoid synchronized hot paths in Java.
    Static accessor vs DI scope
    Need one instance?
    Process-wide resource
    Static getInstance()
    Hidden dependency
    DI singleton scope
    Explicit injection
    Tests inject fake
    Same uniqueness
    Uniqueness is a lifecycle requirement; static access is an architecture choice.
    When Singleton fits
    Process-wide?
    Metrics · config · pool
    Per-request data?
    Stop — wrong scope
    DI container?
    Register as singleton
    Tests need fake?
    Inject interface
    Never store per-user state on a process-wide singleton.

    Informative example

    Before / after — static Singleton vs DI-scoped singleton:

    ts
    // ❌ Hidden dependency — untestable without PowerMock
    class OrderService {
    placeOrder(cart: Cart) {
    Logger.getInstance().info("Placing order");
    PaymentGateway.getInstance().charge(cart.total);
    }
    }
    // ✅ Same uniqueness — explicit dependencies
    interface 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 production
    const app = createApp({
    logger: new WinstonLogger(),
    payments: new StripeGateway(process.env.STRIPE_KEY),
    });

    Execution workflow

    1Singleton lifecycle
    1 / 4

    First access

    Client invokes getInstance() or container resolves bean.

    Static field is null on first call.

    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.

    4 questions
    1IntermediateQuestionWhy is naive lazy Singleton not thread-safe in Java?+

    Answer

    Two threads can both see null, enter the if-block, and construct twice. Fix with holder class, enum, or DCL with volatile.

    Follow-up

    Why does DCL need volatile?
    2AdvancedQuestionWhen pick enum Singleton over Bill Pugh holder?+

    Answer

    When you need serialization and reflection safety for free. Holder when you must implement an interface awkwardly with enum.

    Follow-up

    What does enum buy over static final INSTANCE?
    3BeginnerQuestionHow is Spring @Component different from hand-coded Singleton?+

    Answer

    Both yield one instance per container, but Spring injects via constructor — never static accessor. Tests swap beans freely.

    Follow-up

    What if scope is prototype?
    4IntermediateQuestionWhat's wrong with Singleton for current user?+

    Answer

    Singleton is process-wide; user context is per-request. Causes cross-request leaks and security bugs. Use request-scoped bean or explicit parameter.

    Follow-up

    ThreadLocal in async code?

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.