Skip to content

Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global access point to that instance.

  • Configuration objects that should be shared across the application
  • Connection pools
  • Logging services
  • Caching

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; }
}
// Usage
var config = AppConfiguration.Instance;
Console.WriteLine(config.ConnectionString);

Lazy<T> handles thread safety — the instance is created once, even if multiple threads call Instance simultaneously.

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];
}
}
// Usage
const logger = Logger.getInstance();
logger.log('App started');
// Same instance
const logger2 = Logger.getInstance();
logger2.log('Something happened');
console.log(Logger.getInstance().getLogs().length); // 2

Module-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:

src/config.ts
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 everywhere
import { config } from './config';
console.log(config.port); // same instance every import

This is simpler and more testable than a traditional Singleton class.

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.

For most cases, dependency injection is better than Singletons:

// Register as singleton in DI container
builder.Services.AddSingleton<IAppConfiguration, AppConfiguration>();
// Inject where needed
public class UserController
{
public UserController(IAppConfiguration config) { ... }
}

This gives you:

  • Easy swapping in tests (inject a mock)
  • Clearer dependencies (explicit in constructor)
  • Framework manages lifetime