Singleton Pattern
Singleton Pattern
Section titled “Singleton Pattern”The Singleton pattern ensures a class has only one instance and provides a global access point to that instance.
When to Use
Section titled “When to Use”- Configuration objects that should be shared across the application
- Connection pools
- Logging services
- Caching
C# Implementation
Section titled “C# Implementation”Thread-safe using Lazy<T> (recommended):
public sealed class AppConfiguration{ private static readonly Lazy<AppConfiguration> _instance = new(() => new AppConfiguration());
private AppConfiguration() { // Load config here ConnectionString = Environment.GetEnvironmentVariable("DATABASE_URL") ?? ""; MaxConnections = int.Parse(Environment.GetEnvironmentVariable("MAX_CONN") ?? "10"); }
public static AppConfiguration Instance => _instance.Value;
public string ConnectionString { get; } public int MaxConnections { get; }}
// Usagevar config = AppConfiguration.Instance;Console.WriteLine(config.ConnectionString);Lazy<T> handles thread safety — the instance is created once, even if multiple threads call Instance simultaneously.
TypeScript Implementation
Section titled “TypeScript Implementation”class Logger { private static instance: Logger; private logs: string[] = [];
private constructor() {}
static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; }
log(message: string): void { const entry = `[${new Date().toISOString()}] ${message}`; this.logs.push(entry); console.log(entry); }
getLogs(): string[] { return [...this.logs]; }}
// Usageconst logger = Logger.getInstance();logger.log('App started');
// Same instanceconst logger2 = Logger.getInstance();logger2.log('Something happened');
console.log(Logger.getInstance().getLogs().length); // 2Module-Based Singleton (TypeScript / Node.js)
Section titled “Module-Based Singleton (TypeScript / Node.js)”In JavaScript/TypeScript, ES modules are cached after the first import — making a module-level export a natural singleton:
class Config { readonly dbUrl = process.env.DATABASE_URL ?? 'postgresql://localhost/mydb'; readonly port = parseInt(process.env.PORT ?? '3000', 10);}
export const config = new Config(); // created once, shared everywhereimport { config } from './config';console.log(config.port); // same instance every importThis is simpler and more testable than a traditional Singleton class.
Pitfalls
Section titled “Pitfalls”Global state — Singletons introduce global mutable state, making code harder to test and reason about.
Testing difficulty — Singletons persist between tests. Reset state in afterEach, or use dependency injection instead.
Concurrency — In languages without built-in protection (early Java, naive TypeScript), double-checked locking is needed. Use Lazy<T> in C# or module-level exports in TypeScript.
Prefer Dependency Injection
Section titled “Prefer Dependency Injection”For most cases, dependency injection is better than Singletons:
// Register as singleton in DI containerbuilder.Services.AddSingleton<IAppConfiguration, AppConfiguration>();
// Inject where neededpublic class UserController{ public UserController(IAppConfiguration config) { ... }}This gives you:
- Easy swapping in tests (inject a mock)
- Clearer dependencies (explicit in constructor)
- Framework manages lifetime