One Instance to Rule Them All
What happens when two parts of your application create separate database connection pools, each consuming 100 connections? Or when multiple loggers write to the same file simultaneously, corrupting your logs? The Singleton pattern ensures exactly one instance exists, providing global access while controlling resource usage.
The Problem: Uncontrolled Instance Creation
You’re building an application configuration manager:
public class ConfigurationManager
{
private Dictionary<string, string> _settings = new();
public ConfigurationManager()
{
// Expensive: reads from disk, parses JSON, validates
LoadSettingsFromFile();
}
private void LoadSettingsFromFile()
{
Console.WriteLine("Loading configuration from disk...");
System.Threading.Thread.Sleep(500); // Simulate I/O
_settings["ApiKey"] = "abc123";
_settings["DatabaseUrl"] = "server=localhost";
}
public string GetSetting(string key) => _settings.GetValueOrDefault(key, "");
}
// Different parts of the application
public class UserService
{
private ConfigurationManager _config = new(); // Loads config
public void CreateUser() => _config.GetSetting("DatabaseUrl");
}
public class EmailService
{
private ConfigurationManager _config = new(); // Loads AGAIN!
public void SendEmail() => _config.GetSetting("ApiKey");
}
// Problem: Configuration loaded multiple times, wasting resources!
Problems:
- Multiple instances waste memory and resources
- Expensive initialization repeated unnecessarily
- Inconsistent state if one instance is modified
- No control over when/how instances are created
- Global resources (files, ports, connections) can conflict
The Solution: Singleton Pattern
The Singleton pattern ensures a class has only one instance and provides a global point of access to it.
Definition: Ensure a class has only one instance and provide a global point of access to it.
Pseudocode Structure
Singleton
├── private static instance
├── private constructor (prevents external instantiation)
├── public static GetInstance() : returns the single instance
└── public methods for business logic
Real-World Example: Thread-Safe Singleton Implementations
Implementation 1: Thread-Safe Lazy Initialization (Recommended)
public sealed class ConfigurationManager
{
private static readonly Lazy<ConfigurationManager> _instance =
new Lazy<ConfigurationManager>(() => new ConfigurationManager());
private readonly Dictionary<string, string> _settings = new();
private ConfigurationManager()
{
Console.WriteLine("🔧 Initializing ConfigurationManager...");
LoadSettings();
}
public static ConfigurationManager Instance => _instance.Value;
private void LoadSettings()
{
// Simulate expensive operation
System.Threading.Thread.Sleep(500);
_settings["ApiKey"] = "abc123xyz";
_settings["DatabaseUrl"] = "Server=localhost;Database=MyApp";
_settings["MaxConnections"] = "100";
_settings["Environment"] = "Production";
Console.WriteLine("✅ Configuration loaded");
}
public string GetSetting(string key)
{
return _settings.TryGetValue(key, out var value)
? value
: throw new KeyNotFoundException($"Setting '{key}' not found");
}
public void UpdateSetting(string key, string value)
{
_settings[key] = value;
Console.WriteLine($"📝 Updated {key} = {value}");
}
public void DisplayAllSettings()
{
Console.WriteLine("\n⚙️ Current Configuration:");
foreach (var setting in _settings)
{
Console.WriteLine($" {setting.Key}: {setting.Value}");
}
}
}
Implementation 2: Double-Check Locking (Manual Control)
public sealed class Logger
{
private static Logger? _instance;
private static readonly object _lock = new object();
private readonly List<string> _logs = new();
private Logger()
{
Console.WriteLine("📋 Logger initialized");
}
public static Logger Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Logger();
}
}
}
return _instance;
}
}
public void Log(string message)
{
lock (_lock)
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var logEntry = $"[{timestamp}] {message}";
_logs.Add(logEntry);
Console.WriteLine(logEntry);
}
}
public void LogError(string message)
{
Log($"❌ ERROR: {message}");
}
public void LogInfo(string message)
{
Log($"ℹ️ INFO: {message}");
}
public IReadOnlyList<string> GetLogs() => _logs.AsReadOnly();
}
Implementation 3: Eager Initialization (Simple & Thread-Safe)
public sealed class DatabaseConnectionPool
{
// Instance created at class load time - thread-safe by CLR
private static readonly DatabaseConnectionPool _instance = new();
private readonly List<string> _connections = new();
private const int MaxConnections = 10;
private DatabaseConnectionPool()
{
Console.WriteLine("💾 Initializing Database Connection Pool...");
InitializeConnections();
}
public static DatabaseConnectionPool Instance => _instance;
private void InitializeConnections()
{
for (int i = 0; i < MaxConnections; i++)
{
_connections.Add($"Connection-{i + 1}");
}
Console.WriteLine($"✅ Created {MaxConnections} database connections");
}
public string AcquireConnection()
{
if (_connections.Count == 0)
throw new InvalidOperationException("No connections available");
var connection = _connections[0];
_connections.RemoveAt(0);
Console.WriteLine($"📤 Acquired: {connection} ({_connections.Count} remaining)");
return connection;
}
public void ReleaseConnection(string connection)
{
_connections.Add(connection);
Console.WriteLine($"📥 Released: {connection} ({_connections.Count} available)");
}
public int AvailableConnections => _connections.Count;
}
Usage Example
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("=== Singleton Pattern Demo ===\n");
// ConfigurationManager Singleton
Console.WriteLine("1️⃣ Testing ConfigurationManager Singleton\n");
var config1 = ConfigurationManager.Instance;
var config2 = ConfigurationManager.Instance;
Console.WriteLine($"Same instance? {ReferenceEquals(config1, config2)}");
config1.DisplayAllSettings();
config1.UpdateSetting("ApiKey", "new-key-456");
config2.DisplayAllSettings(); // Shows updated value!
// Logger Singleton
Console.WriteLine("\n\n2️⃣ Testing Logger Singleton\n");
var logger1 = Logger.Instance;
var logger2 = Logger.Instance;
logger1.LogInfo("Application started");
logger2.LogInfo("User logged in");
logger1.LogError("Database connection failed");
Console.WriteLine($"\nTotal logs: {logger2.GetLogs().Count}");
// Database Connection Pool Singleton
Console.WriteLine("\n\n3️⃣ Testing Connection Pool Singleton\n");
var pool1 = DatabaseConnectionPool.Instance;
var pool2 = DatabaseConnectionPool.Instance;
var conn1 = pool1.AcquireConnection();
var conn2 = pool2.AcquireConnection();
Console.WriteLine($"Available: {pool1.AvailableConnections}");
pool1.ReleaseConnection(conn1);
pool2.ReleaseConnection(conn2);
Console.WriteLine($"Available: {pool1.AvailableConnections}");
// Thread Safety Test
Console.WriteLine("\n\n4️⃣ Testing Thread Safety\n");
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
int threadNum = i;
tasks.Add(Task.Run(() =>
{
var config = ConfigurationManager.Instance;
Logger.Instance.LogInfo($"Thread {threadNum} accessed singleton");
}));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine("\n✅ All threads accessed the same singleton instances");
}
}
Advanced Example: Dependency Injection Compatible Singleton
// Modern approach: Singleton via DI container
public interface IAppCache
{
void Set(string key, object value);
object? Get(string key);
}
public class AppCache : IAppCache
{
private readonly Dictionary<string, object> _cache = new();
private readonly object _lock = new();
public void Set(string key, object value)
{
lock (_lock)
{
_cache[key] = value;
Console.WriteLine($"💾 Cached: {key}");
}
}
public object? Get(string key)
{
lock (_lock)
{
return _cache.TryGetValue(key, out var value) ? value : null;
}
}
}
// In Program.cs or Startup.cs
// services.AddSingleton<IAppCache, AppCache>();
// Usage with DI
public class UserService
{
private readonly IAppCache _cache;
public UserService(IAppCache cache)
{
_cache = cache; // DI container ensures singleton
}
public void CacheUser(int id, string name)
{
_cache.Set($"user:{id}", name);
}
}
Anti-Pattern Warning: Global State Issues
// CAREFUL: Singleton can become global state
public class BadSingleton
{
public static BadSingleton Instance { get; } = new();
// Mutable global state - problematic!
public int Counter { get; set; } = 0;
public List<string> SharedList { get; set; } = new();
}
// Multiple components modifying shared state = chaos
service1.BadSingleton.Instance.Counter++;
service2.BadSingleton.Instance.Counter++;
// Hard to track, hard to test, hard to debug
// BETTER: Singleton manages resources, not business state
public class GoodSingleton
{
public static GoodSingleton Instance { get; } = new();
// Provides services, not state
public void LogMessage(string msg) { /* ... */ }
public string GetConfiguration(string key) { /* ... */ }
}
Enterprise Applications
1. Application Configuration
Centralized settings loaded once and accessed throughout the application.
2. Logging Infrastructure
Single logger instance coordinating file writes and log aggregation.
3. Caching Systems
Unified in-memory cache shared across all components.
4. Connection Pools
Manage database connections, HTTP clients, or external service connections.
5. Hardware Interface
Control access to printers, scanners, or other shared physical resources.
Benefits
- Controlled Access: Single point of control for global resources
- Memory Efficiency: One instance instead of many duplicates
- Lazy Initialization: Created only when needed (with Lazy<T>)
- Thread Safety: Properly implemented singletons are thread-safe
- Global Access: Accessible from anywhere in the application
When to Use
Use Singleton when:
- Exactly one instance must exist (configuration, logging, caching)
- The instance needs global access point
- The single instance should be extensible by subclassing
- You’re managing shared resources (connection pools, file handles)
When NOT to Use
Avoid Singleton when:
- You can use dependency injection instead (preferred in modern apps)
- Testing requires multiple instances with different configurations
- The class represents business logic rather than infrastructure
- Global state makes your application harder to reason about
- You’re just trying to avoid passing parameters
Common Pitfalls
Not Thread-Safe:
// WRONG - Race condition!
public static Singleton Instance
{
get
{
if (_instance == null)
_instance = new Singleton(); // Multiple threads!
return _instance;
}
}
// RIGHT - Use Lazy<T>
private static readonly Lazy<Singleton> _instance =
new Lazy<Singleton>(() => new Singleton());
Breaking Singleton via Reflection:
// Prevent via sealed class and private constructor
public sealed class Singleton
{
private Singleton() { }
}
Key Takeaway: The Singleton pattern ensures exactly one instance of a class exists, providing controlled access to shared resources. While powerful, use it judiciously—modern applications often prefer dependency injection with singleton lifetime for better testability and flexibility.
