Creational Patterns • Singleton PatternHard⏱️ ~3 min
Singleton Pattern: When to Use vs Alternatives
When Singleton Is Appropriate:
First, Single Logical Resource: When the domain has exactly one instance conceptually (one parking lot, one application configuration, one hardware device driver).
Second, Controlled Access: When you need to enforce constraints on instance creation (prevent multiple loggers writing to the same file, prevent multiple connection pools exhausting resources).
Third, Lazy Expensive Initialization: When the resource is costly to create (loading large configuration files, establishing network connections) and should be created once and reused.
Fourth, Global Coordination: When components across the application need to coordinate through shared state (cache manager, event bus).
Second, Controlled Access: When you need to enforce constraints on instance creation (prevent multiple loggers writing to the same file, prevent multiple connection pools exhausting resources).
Third, Lazy Expensive Initialization: When the resource is costly to create (loading large configuration files, establishing network connections) and should be created once and reused.
Fourth, Global Coordination: When components across the application need to coordinate through shared state (cache manager, event bus).
Problem: Global Variable
Using a plain global variable allows multiple instances to be created accidentally, provides no lifecycle control, and makes testing difficult.
→
Solution: Singleton
Singleton enforces single instance through private constructor, controls initialization timing, and provides testability through dependency injection of the instance.
When Singleton Is the Wrong Choice:
First: Testing Challenges
Singletons introduce global state that persists across test cases. If
Second: Hidden Dependencies
When a class calls
Third: Tight Coupling
Direct calls to
Fourth: Scalability Constraints
In distributed systems or cloud environments, a singleton per process does not scale. Multiple application instances each have their own singleton, leading to inconsistent state. Alternative: Use external shared storage (Redis cache, centralized configuration service) or accept that each instance has its own local manager.
Singletons introduce global state that persists across test cases. If
ConfigurationManager.getInstance() loads settings during one test, those settings affect subsequent tests unless explicitly reset. Alternative: Use Dependency Injection (DI) to pass configuration objects as constructor parameters. Tests can provide mock configurations without affecting global state.Second: Hidden Dependencies
When a class calls
Logger.getInstance() internally, that dependency is invisible in the constructor signature. Code reviewers and maintainers cannot see what the class depends on. Alternative: Inject the logger through the constructor: OrderProcessor(logger: Logger). Dependencies become explicit and the class becomes easier to understand and test.Third: Tight Coupling
Direct calls to
Singleton.getInstance() couple your code to the concrete singleton class, making it hard to swap implementations. Alternative: Define an interface (e.g., ILogger) and inject implementations. The singleton can still exist but is accessed through abstraction.Fourth: Scalability Constraints
In distributed systems or cloud environments, a singleton per process does not scale. Multiple application instances each have their own singleton, leading to inconsistent state. Alternative: Use external shared storage (Redis cache, centralized configuration service) or accept that each instance has its own local manager.
Warning: Singleton is often overused by beginners as a substitute for proper dependency management. If you find yourself creating many singletons, reconsider your architecture. Most classes should not be singletons.
Alternatives to Singleton:
Dependency Injection (DI): Create one instance at application startup and inject it wherever needed. Frameworks like Spring manage instance lifecycle. The object behaves like a singleton (one instance) but without the static accessor method, improving testability.
Monostate Pattern: All instances share the same static state. Multiple objects exist but behave as one. Use when you need instance-level behavior but shared state. However, this is rare and often more confusing than helpful.
Factory Pattern: Factory controls instance creation and can enforce single instance internally without exposing static methods. Clients request instances from the factory, which returns the same object each time.
Service Locator Pattern: Central registry holds single instances of services. Clients query the registry by service type. This decouples clients from concrete singletons but introduces a new dependency on the locator.
Monostate Pattern: All instances share the same static state. Multiple objects exist but behave as one. Use when you need instance-level behavior but shared state. However, this is rare and often more confusing than helpful.
Factory Pattern: Factory controls instance creation and can enforce single instance internally without exposing static methods. Clients request instances from the factory, which returns the same object each time.
Service Locator Pattern: Central registry holds single instances of services. Clients query the registry by service type. This decouples clients from concrete singletons but introduces a new dependency on the locator.
Interview Tip: When asked about Singleton, always discuss its drawbacks (testing difficulty, tight coupling, global state) and explain when DI is preferable. Strong candidates know the trade-offs.
Decision Framework:
Use Singleton when: The domain truly has one instance AND you need to prevent accidental creation AND the instance is accessed from many unrelated parts of the codebase.
Use Dependency Injection when: You want testability AND explicit dependencies AND flexibility to swap implementations.
Use neither when: Components can create their own instances locally without coordination.
Use Dependency Injection when: You want testability AND explicit dependencies AND flexibility to swap implementations.
Use neither when: Components can create their own instances locally without coordination.
💡 Key Takeaways
✓Singleton is appropriate for single logical resources with controlled access
✓Global variables lack lifecycle control; Singleton provides initialization management
✓Testing becomes harder due to global state persisting across test cases
✓Hidden dependencies reduce code clarity; DI makes dependencies explicit
✓Tight coupling to concrete classes limits flexibility; use interfaces and DI
✓In distributed systems, consider external shared storage instead of per-process singletons
✓Dependency Injection is often superior for testability and maintainability
📌 Examples
1Appropriate: ConfigurationManager loading app settings once
2Inappropriate: UserService as singleton when multiple users exist
3Better with DI: OrderProcessor receiving injected Logger
4Scalability issue: SessionManager singleton in multi-server deployment