Table of contents
Open Table of contents
Problem Statement
Implement a Config singleton that:
- Can only be instantiated once
- Provides a global
getInstance()method - Supports
get(key)andset(key, value)for configuration values - Is lazily initialized (created on first access)
Then discuss: When should you avoid singletons?
Concepts
The singleton pattern ensures a class has only one instance and provides a global access point to it. It’s one of the simplest patterns but also one of the most controversial.
Common uses: Configuration, logging, connection pools, caches.
Problems: Hidden dependencies, global state, difficult to test, tight coupling.
TypeScript/Python: ES modules and Python modules are natural singletons — a module is only loaded once. See discussion below.
Boilerplate
TypeScript
// singleton.ts
class Config {
private static instance: Config | null = null;
private data: Map<string, unknown> = new Map();
private constructor() {}
static getInstance(): Config {
// TODO
throw new Error("Not implemented");
}
get<T>(key: string): T | undefined { /* TODO */ return undefined; }
set(key: string, value: unknown): void { /* TODO */ }
}
Hints
Hint 1: Lazy initialization
In getInstance(), check if instance is null. If so, create it. Always return instance.
Hint 2: Preventing direct instantiation
Make the constructor private (TypeScript) or use __new__ override (Python) to prevent direct new Config() calls.
Solution
TypeScript
class Config {
private static instance: Config | null = null;
private data: Map<string, unknown> = new Map();
private constructor() {}
static getInstance(): Config {
if (!Config.instance) {
Config.instance = new Config();
}
return Config.instance;
}
get<T>(key: string): T | undefined {
return this.data.get(key) as T | undefined;
}
set(key: string, value: unknown): void {
this.data.set(key, value);
}
has(key: string): boolean {
return this.data.has(key);
}
clear(): void {
this.data.clear();
}
}
// Usage
const config = Config.getInstance();
config.set("apiUrl", "https://api.example.com");
config.set("timeout", 5000);
// Same instance everywhere
const sameConfig = Config.getInstance();
sameConfig.get<string>("apiUrl"); // "https://api.example.com"
Python
from typing import Any
class Config:
_instance: "Config | None" = None
_data: dict[str, Any]
def __new__(cls) -> "Config":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._data = {}
return cls._instance
def get(self, key: str, default: Any = None) -> Any:
return self._data.get(key, default)
def set(self, key: str, value: Any) -> None:
self._data[key] = value
def has(self, key: str) -> bool:
return key in self._data
# Usage
config1 = Config()
config1.set("debug", True)
config2 = Config()
config2.get("debug") # True
config1 is config2 # True
PHP
class Config {
private static ?self $instance = null;
private array $data = [];
private function __construct() {}
private function __clone() {}
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function get(string $key, mixed $default = null): mixed {
return $this->data[$key] ?? $default;
}
public function set(string $key, mixed $value): void {
$this->data[$key] = $value;
}
}
When to Avoid Singletons
Singletons introduce hidden global state. Consider these alternatives:
| Problem | Alternative |
|---|---|
| Need shared state | Dependency injection |
| Need one instance | DI container with singleton scope |
| Need global config | Module-level constants or environment variables |
| Need a registry | Pass the registry explicitly |
Testing difficulty: Singletons carry state between tests. You’d need a reset() method or constructor injection to make them testable.
Complexity Analysis
O(1) for all operations. The pattern adds no computational overhead — it’s purely about object lifecycle management.
Cross-Language Notes
TypeScript/JavaScript: ES modules are cached after first import. Exporting an object from a module is effectively a singleton without the boilerplate:
// config.ts export const config = new Map<string, unknown>();Every import gets the same
configinstance.
Python: Same principle — a module-level object is a singleton. Python’s module caching (
sys.modules) ensures a module’s top-level code runs only once:# config.py _config: dict[str, Any] = {} def get(key: str) -> Any: return _config.get(key) def set(key: str, value: Any) -> None: _config[key] = value
PHP: PHP’s shared-nothing architecture (each request starts fresh) means singletons only live within a single request. This actually makes them safer than in long-running processes. In frameworks like Symfony/Laravel, the DI container manages singleton services.
Rust: Rust makes singletons intentionally difficult (requiring
unsafeorOnceCell/LazyLock) to discourage global mutable state. This is a deliberate design choice.