Singleton (and why to avoid it)
A creational pattern that ensures a class has only one instance with a global access point, widely considered an anti-pattern due to hidden dependencies and testing difficulties.
Description
The Singleton pattern restricts a class to a single instance and provides a global access point to it, typically via a static getInstance() method. The instance is created lazily on first access and returned on subsequent calls. While it seems simple and useful—database connections, configuration objects, and loggers are natural candidates—the pattern introduces serious architectural problems that have led the software engineering community to largely regard it as an anti-pattern.
The core problems with Singleton are: it introduces hidden global state (any code can access and modify the singleton without declaring it as a dependency, making data flow opaque), it tightly couples consumers to the concrete singleton class (violating the Dependency Inversion Principle), it makes unit testing extremely difficult (you cannot substitute a mock or stub without special tricks like dependency injection frameworks or module mocking), it prevents parallel test execution (tests share the singleton's mutable state), and it can cause subtle bugs in concurrent environments (lazy initialization race conditions, shared mutable state across threads or async operations).
The modern alternative to Singleton is dependency injection: instead of a class accessing DatabaseConnection.getInstance(), it receives the connection through its constructor. A DI container can be configured to provide the same instance to all consumers (the 'singleton' lifecycle scope in DI terminology), achieving the same single-instance guarantee without the coupling. In Node.js, module-level singletons (export const db = createConnection()) are common and slightly better than class-based singletons because they are easier to mock (jest.mock), but they still create hidden dependencies. The best practice is to use DI for any dependency that you might want to replace during testing.
Prompt Snippet
Refactor the existing DatabaseConnection.getInstance() singleton to use dependency injection: define an IDatabaseConnection interface, implement it with PostgresConnection using the pg Pool, and register it in the tsyringe container as a singleton-scoped dependency (container.registerSingleton<IDatabaseConnection>(TOKENS.Database, PostgresConnection)). This preserves the single-instance guarantee while enabling test substitution via container.register(TOKENS.Database, { useValue: mockDb }). For the logger, use module-level scoping (export const logger = pino({...})) with dependency injection for services that need testable logging behavior. Add an ESLint rule (no-restricted-syntax) to flag any static getInstance() method declarations in the codebase.Tags
Related Terms
Dependency Injection
A design pattern where objects receive their dependencies from an external source rather than creating them internally, enabling loose coupling and testability.
Factory Pattern
A creational design pattern that encapsulates object creation logic, allowing the client to request objects without knowing the concrete class or complex construction details.
Repository Pattern
An abstraction layer that mediates between the domain/business logic and the data persistence layer, providing a collection-like interface for accessing domain objects.
Service Layer Pattern
An architectural layer that defines the application's boundary and coordinates domain logic, transaction management, and cross-cutting concerns through service classes.