Dependency Injection vs Singleton
Dependency Injection vs Singleton clarifies an common confusion: DI and Singleton solve overlapping but distinct problems.
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):
@Injectable({ scope: Scope.DEFAULT }) // singletonclass 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:
Informative example
Before / after — static Singleton vs scoped DI:
// ❌ Static — uniqueness yes, testability noclass FeatureFlagClient {private static instance: FeatureFlagClientstatic 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
Declare scopes
Mark each bean: singleton, request, transient.
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.
1IntermediateQuestionWhy Singleton still in interview decks if DI exists?+
Answer
Follow-up
2IntermediateQuestionHow do scopes change the trade-off?+
Answer
Follow-up
3AdvancedQuestionConstructor vs field injection?+
Answer
Follow-up
4AdvancedQuestionWhat is composition root?+
Answer
Follow-up
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.