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

    Dependency Injection vs Singleton

    Dependency Injection vs Singleton clarifies an common confusion: DI and Singleton solve overlapping but distinct problems.

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

    Introduction

    Dependency Injection vs Singleton clarifies an common confusion: DI and Singleton solve overlapping but distinct problems. Singleton enforces uniqueness via static accessor. DI enforces explicit dependencies via constructor parameters. Modern apps want both — register singleton-scoped beans in a DI container and inject them.

    The story

    A team lead asked: "Should FeatureFlagClient be a Singleton?" The architect answered: "Register it as a singleton-scoped bean. One instance per process AND constructor-injected dependencies. Avoid the static accessor — not the uniqueness."

    The business problem

    Conflating "one instance" with "static access" blocks testability and obscures architecture:

    • False dichotomy: teams think DI means many instances of everything.
    • Flaky tests: hand-rolled Singletons leak state; DI containers swap fakes cleanly.
    • Hidden cycles: static accessors defer cycle detection to first runtime call.
    • Wrong scope: prototype everything — expensive resources recreated per request.

    The problem teams faced

    Clarify what each mechanism owns:

    • Uniqueness (lifecycle) — how many instances per process/request.
    • Access path (architecture) — static vs constructor injection.
    • Scope selection — singleton infra vs request-scoped domain state.
    • Composition root — who builds the graph and when.

    Understanding the topic

    Intent: Choose the right mechanism for uniqueness vs explicit dependencies.

    • Singleton scope — one instance per container/process (Logger, Config, Pool).
    • Request scope — one instance per HTTP request (UserContext, TenantFactory).
    • Prototype scope — new instance every injection (stateful handlers).
    • Composition root — bootstrap wires scopes; app code stays framework-agnostic.

    Internal architecture

    Scoped DI registration (NestJS-style):

    ts
    @Injectable({ scope: Scope.DEFAULT }) // singleton
    class LoggerService { /* one per process */ }
    @Injectable({ scope: Scope.REQUEST })
    class RequestContext { /* one per HTTP request */ }
    @Module({
    providers: [LoggerService, RequestContext, OrderService],
    })
    export class AppModule {}

    Visual explanation

    Three diagrams separate uniqueness, access path, and scopes:

    Uniqueness vs access path
    One instance needed?
    Lifecycle question
    Static getInstance()
    Bad access path
    DI singleton scope
    Good access path
    Same uniqueness
    Better testability
    Interview answer: uniqueness ≠ static access.
    Scope selection
    Logger / Config
    Singleton scope
    Current user
    Request scope
    Command handler
    Prototype optional
    Wrong scope
    Data leak or waste
    Match scope to lifecycle — not everything is singleton.
    Composition root
    App bootstrap
    Single wiring place
    Register scopes
    singleton · request
    Resolve graph
    Cycle check at boot
    Handlers stay clean
    No framework in domain
    Composition root is architecture — not an afterthought.

    Informative example

    Before / after — static Singleton vs scoped DI:

    ts
    // ❌ Static — uniqueness yes, testability no
    class FeatureFlagClient {
    private static instance: FeatureFlagClient
    static getInstance() { return this.instance ??= new FeatureFlagClient() }
    isEnabled(flag: string) { /* calls remote service */ }
    }
    class OrderService {
    placeOrder() {
    if (FeatureFlagClient.getInstance().isEnabled("new-checkout")) { /* ... */ }
    }
    }
    // ✅ DI singleton scope — same uniqueness, injectable fake
    @Injectable()
    class FeatureFlagClient {
    isEnabled(flag: string) { /* calls remote service */ }
    }
    @Injectable()
    class OrderService {
    constructor(private flags: FeatureFlagClient) {}
    placeOrder() {
    if (this.flags.isEnabled("new-checkout")) { /* ... */ }
    }
    }
    // Test: { provide: FeatureFlagClient, useValue: { isEnabled: () => true } }

    Execution workflow

    1DI + singleton scope lifecycle
    1 / 4

    Declare scopes

    Mark each bean: singleton, request, transient.

    Document in ADR per collaborator type.

    Real-world use

    Spring singleton beans, NestJS default Injectable scope, Angular hierarchical injector, .NET AddSingleton, Guice @Singleton. None use static accessors — all guarantee scoped uniqueness with explicit injection.

    Production case study

    Five-year Java app migration:

    • Scenario: Hand-rolled Singletons across monolith.
    • Decision: One module per sprint; getInstance() delegates to ApplicationContext during transition.
    • Outcome: Test runtime down 4×; two circular deps surfaced and fixed at boot.
    • Lesson: DI singleton scope replaces static — not uniqueness itself.

    Trade-offs

    • Pro: uniqueness + explicit deps + cycle detection + test fakes.
    • Con: requires container or disciplined manual composition root.
    • Con: request scope needs lifecycle awareness in async/reactive code.

    Decision framework

    • Use DI singleton scope for process-wide infra (logger, config, pools).
    • Use request scope for per-call state (user, tenant, trace context).
    • Avoid static accessors in all application code — reserve for true constants.
    • Avoid prototype scope for expensive resources unless state requires it.

    Best practices

    • Constructor injection only — final/readonly fields, visible dependencies.
    • One composition root per entry point (web, CLI, worker).
    • Document scope per bean in module README or ADR.

    Anti-patterns to avoid

    • Singleton pattern in new greenfield code — use DI scope instead.
    • Field injection — hides dependencies; breaks immutability.
    • Service Locator with DI container — still hidden lookup.

    Common mistakes

    • Request-scoped bean injected into singleton — scope leak / stale context.
    • Async code losing request scope — propagate context explicitly.
    • Keeping getInstance() delegate forever — finish migration.

    Debugging tips

    • Wrong user data in logs → check request-scoped bean injected into singleton.
    • Container startup cycle error → draw dependency graph from constructor signatures.

    Optimization strategies

    • Lazy singleton init for expensive beans not needed in every deployment profile.
    • Compile-time DI (Dagger, Spring AOT) when reflection cost matters on hot paths.

    Common misconceptions

    • DI ≠ many instances. Default scope in most frameworks is singleton.
    • Singleton pattern still in interviews to test understanding of uniqueness vs static access.
    • Every @Bean method is Factory Method registered with a scope.

    Advanced interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    4 questions
    1IntermediateQuestionWhy Singleton still in interview decks if DI exists?+

    Answer

    Teaching example for one-instance mechanics and hazards. Expected answer: understand mechanic AND prefer DI scope to express it.

    Follow-up

    Hazard static has that DI singleton lacks?
    2IntermediateQuestionHow do scopes change the trade-off?+

    Answer

    Container offers app/request/transaction/call scopes. Wrong scope leaks user data or wastes resources. Static Singleton only offers process-wide.

    Follow-up

    Scope for auth context?
    3AdvancedQuestionConstructor vs field injection?+

    Answer

    Constructor: visible deps, immutable fields, no framework needed in tests. Field injection only advantage is brevity — not worth it.

    Follow-up

    When field injection acceptable?
    4AdvancedQuestionWhat is composition root?+

    Answer

    Single bootstrap place where object graph is wired. Centralizes 'where deps come from'; rest of app stays framework-agnostic.

    Follow-up

    Multiple entry points?

    Summary

    You can explain why DI singleton scope replaces static Singleton, pick scopes correctly, and describe composition root discipline. Tell the FeatureFlagClient architect story — if you can do that, you own this lesson and the Creational section.

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