The Singleton design pattern ensures that a class has only one instance throughout the application’s lifetime and provides global access to that instance. While it’s one of the most well-known design patterns, it’s also one of the most controversial due to potential issues with testability, thread safety, and coupling.
In this comprehensive guide, we’ll explore various implementations of the Singleton pattern in C#, from basic approaches to thread-safe versions, and discuss modern alternatives using dependency injection containers.
Table of Contents
Open Table of Contents
- What is the Singleton Pattern?
- Basic Singleton Implementation
- Thread-Safe Singleton Implementations
- Real-World Singleton Examples
- Generic Singleton Base Class
- Modern Alternatives: Dependency Injection
- Testing Singleton Classes
- Performance Considerations
- Best Practices and Guidelines
- Common Pitfalls and How to Avoid Them
- Conclusion
What is the Singleton Pattern?
The Singleton pattern restricts the instantiation of a class to a single instance and provides a global point of access to that instance. It’s useful when you need exactly one instance of a class to coordinate actions across your application.
Common Use Cases
- Configuration Management: Application settings that should be shared globally
- Logging Services: Centralized logging throughout the application
- Database Connection Pools: Managing shared database connections
- Cache Managers: Application-wide caching mechanisms
- Hardware Interface Access: Printer spoolers, file systems
Key Characteristics
- Single Instance: Only one instance can exist
- Global Access: Accessible from anywhere in the application
- Lazy or Eager Initialization: Instance created when needed or at startup
- Thread Safety: Must handle concurrent access properly
Basic Singleton Implementation
Let’s start with the simplest implementation and progressively improve it:
1. Naive Implementation (Not Thread-Safe)
public class NaiveSingleton
{
private static NaiveSingleton _instance;
// Private constructor prevents external instantiation
private NaiveSingleton()
{
Console.WriteLine("NaiveSingleton instance created");
}
public static NaiveSingleton Instance
{
get
{
if (_instance == null)
{
_instance = new NaiveSingleton();
}
return _instance;
}
}
public void DoSomething()
{
Console.WriteLine("Doing something with the singleton instance");
}
}
// Usage
var singleton1 = NaiveSingleton.Instance;
var singleton2 = NaiveSingleton.Instance;
Console.WriteLine(ReferenceEquals(singleton1, singleton2)); // True
Problems with this approach:
- Not thread-safe: Multiple threads can create multiple instances
- Race conditions: Two threads might both see
_instance
as null
Thread-Safe Singleton Implementations
2. Simple Thread-Safe Version (Lock-Based)
public class ThreadSafeSingleton
{
private static ThreadSafeSingleton _instance;
private static readonly object _lock = new object();
private ThreadSafeSingleton()
{
Console.WriteLine("ThreadSafeSingleton instance created");
}
public static ThreadSafeSingleton Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
_instance = new ThreadSafeSingleton();
}
return _instance;
}
}
}
public void DoSomething()
{
Console.WriteLine("Thread-safe singleton doing something");
}
}
Improvement: Thread-safe, but has performance overhead due to locking on every access.
3. Double-Checked Locking Pattern
public class DoubleCheckedLockingSingleton
{
private static DoubleCheckedLockingSingleton _instance;
private static readonly object _lock = new object();
private DoubleCheckedLockingSingleton()
{
Console.WriteLine("DoubleCheckedLockingSingleton instance created");
}
public static DoubleCheckedLockingSingleton Instance
{
get
{
// First check without locking
if (_instance == null)
{
lock (_lock)
{
// Second check with locking
if (_instance == null)
{
_instance = new DoubleCheckedLockingSingleton();
}
}
}
return _instance;
}
}
public void DoSomething()
{
Console.WriteLine("Double-checked locking singleton doing something");
}
}
Benefits: Reduces locking overhead while maintaining thread safety.
4. Lazy Implementation (Recommended)
public class LazySingleton
{
private static readonly Lazy<LazySingleton> _lazy =
new Lazy<LazySingleton>(() => new LazySingleton());
private LazySingleton()
{
Console.WriteLine("LazySingleton instance created");
}
public static LazySingleton Instance => _lazy.Value;
public void DoSomething()
{
Console.WriteLine("Lazy singleton doing something");
}
}
Benefits:
- Thread-safe by default
- Lazy initialization
- Clean and simple code
- No explicit locking required
5. Static Constructor Implementation
public class StaticConstructorSingleton
{
private static readonly StaticConstructorSingleton _instance = new StaticConstructorSingleton();
// Static constructor ensures thread safety
static StaticConstructorSingleton()
{
Console.WriteLine("Static constructor called");
}
private StaticConstructorSingleton()
{
Console.WriteLine("StaticConstructorSingleton instance created");
}
public static StaticConstructorSingleton Instance => _instance;
public void DoSomething()
{
Console.WriteLine("Static constructor singleton doing something");
}
}
Benefits:
- Thread-safe
- Simple implementation
- Note: Eager initialization (instance created at class load time)
Real-World Singleton Examples
Configuration Manager
public class ConfigurationManager
{
private static readonly Lazy<ConfigurationManager> _lazy =
new Lazy<ConfigurationManager>(() => new ConfigurationManager());
private readonly Dictionary<string, string> _settings;
private ConfigurationManager()
{
_settings = LoadConfiguration();
}
public static ConfigurationManager Instance => _lazy.Value;
private Dictionary<string, string> LoadConfiguration()
{
// Simulate loading configuration from file/database
return new Dictionary<string, string>
{
{ "DatabaseConnectionString", "Server=localhost;Database=MyApp;" },
{ "ApiBaseUrl", "https://api.myapp.com" },
{ "MaxRetryAttempts", "3" }
};
}
public string GetSetting(string key)
{
return _settings.TryGetValue(key, out var value) ? value : null;
}
public void SetSetting(string key, string value)
{
_settings[key] = value;
}
public T GetSetting<T>(string key, T defaultValue = default)
{
if (!_settings.TryGetValue(key, out var stringValue))
return defaultValue;
try
{
return (T)Convert.ChangeType(stringValue, typeof(T));
}
catch
{
return defaultValue;
}
}
}
// Usage
var config = ConfigurationManager.Instance;
var connectionString = config.GetSetting("DatabaseConnectionString");
var maxRetries = config.GetSetting<int>("MaxRetryAttempts", 1);
Logger Service
public class Logger
{
private static readonly Lazy<Logger> _lazy = new Lazy<Logger>(() => new Logger());
private readonly object _lock = new object();
private readonly List<string> _logs = new List<string>();
private Logger() { }
public static Logger Instance => _lazy.Value;
public void LogInfo(string message)
{
Log("INFO", message);
}
public void LogWarning(string message)
{
Log("WARNING", message);
}
public void LogError(string message)
{
Log("ERROR", message);
}
public void LogError(Exception exception)
{
Log("ERROR", $"{exception.Message}\n{exception.StackTrace}");
}
private void Log(string level, string message)
{
var logEntry = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
lock (_lock)
{
_logs.Add(logEntry);
Console.WriteLine(logEntry);
// In real implementation, write to file or external service
WriteToFile(logEntry);
}
}
private void WriteToFile(string logEntry)
{
// Simulate file writing
// File.AppendAllTextAsync("app.log", logEntry + Environment.NewLine);
}
public IReadOnlyList<string> GetLogs()
{
lock (_lock)
{
return new List<string>(_logs);
}
}
public void ClearLogs()
{
lock (_lock)
{
_logs.Clear();
}
}
}
// Usage
Logger.Instance.LogInfo("Application started");
Logger.Instance.LogWarning("Low disk space detected");
Logger.Instance.LogError(new InvalidOperationException("Something went wrong"));
var allLogs = Logger.Instance.GetLogs();
Database Connection Manager
public class DatabaseConnectionManager
{
private static readonly Lazy<DatabaseConnectionManager> _lazy =
new Lazy<DatabaseConnectionManager>(() => new DatabaseConnectionManager());
private readonly ConcurrentQueue<IDbConnection> _connectionPool;
private readonly string _connectionString;
private readonly int _maxPoolSize;
private DatabaseConnectionManager()
{
_connectionString = ConfigurationManager.Instance.GetSetting("DatabaseConnectionString");
_maxPoolSize = ConfigurationManager.Instance.GetSetting<int>("MaxPoolSize", 10);
_connectionPool = new ConcurrentQueue<IDbConnection>();
InitializePool();
}
public static DatabaseConnectionManager Instance => _lazy.Value;
private void InitializePool()
{
for (int i = 0; i < _maxPoolSize; i++)
{
var connection = CreateConnection();
_connectionPool.Enqueue(connection);
}
}
private IDbConnection CreateConnection()
{
// In real implementation, return actual database connection
return new SqlConnection(_connectionString);
}
public IDbConnection GetConnection()
{
if (_connectionPool.TryDequeue(out var connection))
{
if (connection.State != ConnectionState.Open)
{
connection.Open();
}
return connection;
}
// If pool is empty, create new connection
return CreateConnection();
}
public void ReturnConnection(IDbConnection connection)
{
if (connection != null && connection.State == ConnectionState.Open)
{
_connectionPool.Enqueue(connection);
}
}
public int AvailableConnections => _connectionPool.Count;
}
// Usage
var dbManager = DatabaseConnectionManager.Instance;
var connection = dbManager.GetConnection();
try
{
// Use connection for database operations
// var command = connection.CreateCommand();
// ...
}
finally
{
dbManager.ReturnConnection(connection);
}
Generic Singleton Base Class
For scenarios where you need multiple singleton classes, you can create a generic base:
public abstract class Singleton<T> where T : class, new()
{
private static readonly Lazy<T> _lazy = new Lazy<T>(() => new T());
public static T Instance => _lazy.Value;
}
// Usage
public class MyService : Singleton<MyService>
{
public void DoWork()
{
Console.WriteLine("MyService doing work");
}
}
public class AnotherService : Singleton<AnotherService>
{
public void ProcessData()
{
Console.WriteLine("AnotherService processing data");
}
}
// Usage
MyService.Instance.DoWork();
AnotherService.Instance.ProcessData();
Modern Alternatives: Dependency Injection
While Singleton pattern works, modern C# applications often use dependency injection containers instead:
Using Microsoft.Extensions.DependencyInjection
// Service interface
public interface IConfigurationService
{
string GetSetting(string key);
T GetSetting<T>(string key, T defaultValue = default);
}
// Service implementation
public class ConfigurationService : IConfigurationService
{
private readonly Dictionary<string, string> _settings;
public ConfigurationService()
{
_settings = LoadConfiguration();
}
private Dictionary<string, string> LoadConfiguration()
{
return new Dictionary<string, string>
{
{ "DatabaseConnectionString", "Server=localhost;Database=MyApp;" },
{ "ApiBaseUrl", "https://api.myapp.com" },
{ "MaxRetryAttempts", "3" }
};
}
public string GetSetting(string key)
{
return _settings.TryGetValue(key, out var value) ? value : null;
}
public T GetSetting<T>(string key, T defaultValue = default)
{
if (!_settings.TryGetValue(key, out var stringValue))
return defaultValue;
try
{
return (T)Convert.ChangeType(stringValue, typeof(T));
}
catch
{
return defaultValue;
}
}
}
// Registration in Program.cs or Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Register as singleton using DI container
services.AddSingleton<IConfigurationService, ConfigurationService>();
// Register other services that depend on it
services.AddScoped<IUserService, UserService>();
}
// Usage in controllers or services
public class UserController : ControllerBase
{
private readonly IConfigurationService _configService;
public UserController(IConfigurationService configService)
{
_configService = configService;
}
[HttpGet]
public IActionResult GetSettings()
{
var apiUrl = _configService.GetSetting("ApiBaseUrl");
return Ok(new { ApiUrl = apiUrl });
}
}
Benefits of DI over Traditional Singleton
- Better Testability: Easy to mock dependencies
- Loose Coupling: Dependencies are injected, not hardcoded
- Lifecycle Management: Container manages object lifetime
- Configuration: Lifetime can be changed without code modification
Testing Singleton Classes
Testing singleton classes can be challenging. Here are some strategies:
1. Making Singleton Testable
public interface ILogger
{
void LogInfo(string message);
void LogError(string message);
}
public class Logger : ILogger
{
private static readonly Lazy<Logger> _lazy = new Lazy<Logger>(() => new Logger());
private readonly List<string> _logs = new List<string>();
private Logger() { }
public static Logger Instance => _lazy.Value;
public void LogInfo(string message)
{
Log("INFO", message);
}
public void LogError(string message)
{
Log("ERROR", message);
}
private void Log(string level, string message)
{
var logEntry = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
_logs.Add(logEntry);
Console.WriteLine(logEntry);
}
// For testing purposes
internal IReadOnlyList<string> GetLogs() => _logs.AsReadOnly();
internal void ClearLogs() => _logs.Clear();
}
// Service that uses logger
public class UserService
{
private readonly ILogger _logger;
public UserService(ILogger logger = null)
{
_logger = logger ?? Logger.Instance;
}
public void CreateUser(string username)
{
_logger.LogInfo($"Creating user: {username}");
// User creation logic
}
}
2. Unit Tests
[Test]
public void UserService_CreateUser_LogsCorrectMessage()
{
// Arrange
var mockLogger = new Mock<ILogger>();
var userService = new UserService(mockLogger.Object);
// Act
userService.CreateUser("testuser");
// Assert
mockLogger.Verify(l => l.LogInfo("Creating user: testuser"), Times.Once);
}
[Test]
public void Logger_Instance_ReturnsSameInstance()
{
// Arrange & Act
var logger1 = Logger.Instance;
var logger2 = Logger.Instance;
// Assert
Assert.AreSame(logger1, logger2);
}
[Test]
public void Logger_LogInfo_AddsMessageToLogs()
{
// Arrange
var logger = Logger.Instance;
logger.ClearLogs(); // Clear previous test logs
// Act
logger.LogInfo("Test message");
// Assert
var logs = logger.GetLogs();
Assert.That(logs.Count, Is.EqualTo(1));
Assert.That(logs[0], Does.Contain("Test message"));
Assert.That(logs[0], Does.Contain("[INFO]"));
}
Performance Considerations
Benchmarking Different Implementations
[MemoryDiagnoser]
public class SingletonBenchmark
{
private const int IterationCount = 1000000;
[Benchmark]
public void LazySingleton_Performance()
{
for (int i = 0; i < IterationCount; i++)
{
var instance = LazySingleton.Instance;
}
}
[Benchmark]
public void DoubleCheckedLocking_Performance()
{
for (int i = 0; i < IterationCount; i++)
{
var instance = DoubleCheckedLockingSingleton.Instance;
}
}
[Benchmark]
public void StaticConstructor_Performance()
{
for (int i = 0; i < IterationCount; i++)
{
var instance = StaticConstructorSingleton.Instance;
}
}
}
Results typically show:
Lazy<T>
implementation has excellent performance- Static constructor is fastest for repeated access
- Double-checked locking has slight overhead
Best Practices and Guidelines
1. When to Use Singleton
✅ Good Use Cases:
- Configuration management
- Logging services
- Hardware access (printers, file system)
- Cache management
- Thread pools
❌ Avoid Singleton When:
- You need multiple instances in the future
- The class has mutable state that varies per context
- You’re using it just to avoid passing parameters
- Testing is difficult due to global state
2. Implementation Guidelines
public class BestPracticeSingleton
{
// Use Lazy<T> for thread-safe lazy initialization
private static readonly Lazy<BestPracticeSingleton> _lazy =
new Lazy<BestPracticeSingleton>(() => new BestPracticeSingleton());
// Private constructor to prevent external instantiation
private BestPracticeSingleton()
{
// Initialize resources here
InitializeResources();
}
// Public property for accessing the instance
public static BestPracticeSingleton Instance => _lazy.Value;
// Initialize any required resources
private void InitializeResources()
{
// Initialization logic
}
// Public methods for singleton functionality
public void DoWork()
{
// Implementation
}
// Implement IDisposable if needed for cleanup
public void Dispose()
{
// Cleanup resources
}
}
3. Error Handling
public class RobustSingleton
{
private static readonly Lazy<RobustSingleton> _lazy =
new Lazy<RobustSingleton>(CreateInstance);
private static RobustSingleton CreateInstance()
{
try
{
return new RobustSingleton();
}
catch (Exception ex)
{
// Log the error
Console.WriteLine($"Failed to create singleton instance: {ex.Message}");
// You might want to retry or throw a more specific exception
throw new SingletonCreationException("Failed to initialize singleton", ex);
}
}
private RobustSingleton()
{
// Initialization that might fail
InitializeWithPossibleFailure();
}
public static RobustSingleton Instance => _lazy.Value;
private void InitializeWithPossibleFailure()
{
// Simulate initialization that might throw
if (DateTime.Now.Millisecond % 2 == 0)
{
throw new InvalidOperationException("Random initialization failure");
}
}
}
public class SingletonCreationException : Exception
{
public SingletonCreationException(string message, Exception innerException)
: base(message, innerException)
{
}
}
Common Pitfalls and How to Avoid Them
1. The Problem with Inheritance
// ❌ Don't do this - Singleton with inheritance
public class BaseSingleton
{
private static BaseSingleton _instance;
protected BaseSingleton() { }
public static BaseSingleton Instance
{
get
{
if (_instance == null)
_instance = new BaseSingleton();
return _instance;
}
}
}
public class DerivedSingleton : BaseSingleton
{
// This breaks the singleton pattern!
private DerivedSingleton() : base() { }
}
// ✅ Better approach - Use composition over inheritance
public interface ISingletonService
{
void DoWork();
}
public class SingletonService : ISingletonService
{
private static readonly Lazy<SingletonService> _lazy =
new Lazy<SingletonService>(() => new SingletonService());
private SingletonService() { }
public static SingletonService Instance => _lazy.Value;
public void DoWork()
{
Console.WriteLine("Doing work in singleton service");
}
}
2. Serialization Issues
[Serializable]
public class SerializableSingleton : ISerializable
{
private static readonly Lazy<SerializableSingleton> _lazy =
new Lazy<SerializableSingleton>(() => new SerializableSingleton());
private SerializableSingleton() { }
public static SerializableSingleton Instance => _lazy.Value;
// Prevent multiple instances during deserialization
protected SerializableSingleton(SerializationInfo info, StreamingContext context)
{
// Return existing instance instead of creating new one
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
// Serialization logic
}
// Ensure singleton pattern during deserialization
private object ReadResolve()
{
return Instance;
}
}
Conclusion
The Singleton pattern is a powerful tool when used appropriately, but it should be applied judiciously. Here are the key takeaways:
Recommended Approaches:
- Use
Lazy<T>
for most singleton implementations - it’s thread-safe, performant, and clean - Consider Dependency Injection as a modern alternative for better testability and flexibility
- Keep singletons stateless or immutable when possible
- Implement proper error handling during initialization
Best Practices:
- Prefer dependency injection over traditional singleton pattern
- Use interfaces to make singletons testable
- Avoid singleton for classes that might need multiple instances
- Consider thread safety from the beginning
- Document why you chose singleton pattern
Modern C# Approach:
// Traditional singleton
var config = ConfigurationManager.Instance;
// Modern DI approach (preferred)
public class MyService
{
private readonly IConfigurationService _config;
public MyService(IConfigurationService config)
{
_config = config;
}
}
The Singleton pattern remains relevant in C#, but modern dependency injection containers often provide better alternatives for managing object lifetimes while maintaining the benefits of single-instance objects with improved testability and flexibility.
Remember: the goal is not just to implement a pattern, but to solve real problems in your application architecture effectively and maintainably.