Singleton Pattern Real-World Example in C#: Complete Implementation
The Singleton pattern is often discussed in abstract terms. This article presents one acceptable scenario—a configuration manager—where Singleton is defensible. It is not a default recommendation or a template to copy; it is contextual justification for when Singleton fits. For most new applications, dependency injection with singleton lifetime remains the preferred approach.
If you're new to the Singleton pattern, start with The Big List of Design Patterns - Everything You Need to Know for an overview of all design pattern categories and how Singleton fits into the creational patterns family.
Problem: Configuration Management
Imagine you're building an application that needs to load configuration from multiple sources (environment variables, configuration files, and command-line arguments) and make it available throughout the application. You need:
- Single source of truth for configuration
- Thread-safe access from multiple threads
- Lazy initialization (only load when needed)
- Error handling for missing or invalid configuration
- Easy access from anywhere in the application
This is one of the few scenarios where Singleton is a reasonable choice—not the default, but defensible when the constraints align.
Solution: Configuration Manager Singleton
We'll implement a ConfigurationManager Singleton that loads configuration from multiple sources and provides thread-safe access. This real-world example demonstrates practical Singleton pattern implementation in C#.
Step 1: Define the Configuration Manager Interface
First, define an interface to make the Singleton testable and flexible:
public interface IConfigurationManager
{
string GetSetting(string key);
string GetSetting(string key, string defaultValue);
bool TryGetSetting(string key, out string value);
bool HasSetting(string key);
void Reload();
}
Why use an interface: Even though we're implementing Singleton, using an interface allows testing and provides flexibility for future changes.
Step 2: Implement the Singleton Configuration Manager
The implementation combines the Singleton pattern with thread-safe initialization using Lazy<T>. This approach ensures only one instance exists while providing safe concurrent access. The implementation includes proper error handling, multiple configuration source support, and an interface for testability.
Now implement the Singleton pattern with thread-safe initialization:
public sealed class ConfigurationManager : IConfigurationManager
{
// Singleton instance using Lazy<T> for thread safety
private static readonly Lazy<ConfigurationManager> instance =
new Lazy<ConfigurationManager>(() => new ConfigurationManager());
public static ConfigurationManager Instance => instance.Value;
public static IConfigurationManager InstanceAsInterface => instance.Value;
private readonly Dictionary<string, string> _settings;
private readonly object _lockObject = new object();
// Private constructor prevents external instantiation
private ConfigurationManager()
{
_settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
LoadConfiguration();
}
public string GetSetting(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("Key cannot be null or empty", nameof(key));
}
lock (_lockObject)
{
if (_settings.TryGetValue(key, out string value))
{
return value;
}
throw new KeyNotFoundException($"Configuration setting '{key}' not found");
}
}
public string GetSetting(string key, string defaultValue)
{
if (string.IsNullOrWhiteSpace(key))
{
return defaultValue;
}
lock (_lockObject)
{
return _settings.TryGetValue(key, out string value) ? value : defaultValue;
}
}
public bool TryGetSetting(string key, out string value)
{
value = null;
if (string.IsNullOrWhiteSpace(key))
{
return false;
}
lock (_lockObject)
{
return _settings.TryGetValue(key, out value);
}
}
public bool HasSetting(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
return false;
}
lock (_lockObject)
{
return _settings.ContainsKey(key);
}
}
public void Reload()
{
lock (_lockObject)
{
_settings.Clear();
LoadConfiguration();
}
}
private void LoadConfiguration()
{
// Load from environment variables
LoadFromEnvironment();
// Load from configuration file
LoadFromFile("appsettings.json");
// Load from command-line arguments (if available)
LoadFromCommandLine();
}
private void LoadFromEnvironment()
{
// Load all environment variables as configuration
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
{
string key = entry.Key.ToString();
string value = entry.Value?.ToString();
if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(value))
{
_settings[key] = value;
}
}
}
private void LoadFromFile(string filePath)
{
if (!File.Exists(filePath))
{
return; // File doesn't exist, skip
}
try
{
string json = File.ReadAllText(filePath);
var config = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
if (config != null)
{
foreach (var kvp in config)
{
// File settings override environment variables
_settings[kvp.Key] = kvp.Value;
}
}
}
catch (JsonException ex)
{
// Log error but don't fail initialization
Console.WriteLine($"Warning: Failed to load configuration from {filePath}: {ex.Message}");
}
}
private void LoadFromCommandLine()
{
// Example: Load from command-line arguments
// In a real application, you'd parse command-line args
// This is a simplified example
string[] args = Environment.GetCommandLineArgs();
for (int i = 0; i < args.Length - 1; i++)
{
if (args[i].StartsWith("--") && i + 1 < args.Length)
{
string key = args[i].Substring(2); // Remove "--"
string value = args[i + 1];
_settings[key] = value;
}
}
}
}
Key features of this implementation:
- Thread-safe Singleton using
Lazy<T> - Private constructor prevents external instantiation
- Sealed class prevents inheritance
- Interface implementation for testability
- Thread-safe access with locking
- Error handling for missing files
- Multiple configuration sources with precedence
Step 3: Usage Examples
Understanding how to use the Singleton in practice helps clarify its value and demonstrates common usage patterns. These examples show different scenarios where the ConfigurationManager Singleton provides convenient access to configuration throughout your application.
Here's how to use the ConfigurationManager Singleton in your application:
// Example 1: Get a required setting
public class DatabaseService
{
public void Connect()
{
string connectionString = ConfigurationManager.Instance.GetSetting("DatabaseConnectionString");
// Use connection string...
}
}
// Example 2: Get a setting with default value
public class ApiClient
{
public void Initialize()
{
string apiUrl = ConfigurationManager.Instance.GetSetting("ApiUrl", "https://api.default.com");
int timeout = int.Parse(ConfigurationManager.Instance.GetSetting("Timeout", "30"));
// Use settings...
}
}
// Example 3: Check if setting exists
public class FeatureToggle
{
public bool IsFeatureEnabled(string featureName)
{
string key = $"Feature.{featureName}.Enabled";
return ConfigurationManager.Instance.HasSetting(key) &&
bool.Parse(ConfigurationManager.Instance.GetSetting(key, "false"));
}
}
// Example 4: Use interface for better testability
public class OrderService
{
private readonly IConfigurationManager _config;
public OrderService(IConfigurationManager config = null)
{
_config = config ?? ConfigurationManager.InstanceAsInterface;
}
public void ProcessOrder(Order order)
{
string taxRate = _config.GetSetting("TaxRate", "0.10");
// Process order with tax rate...
}
}
Step 4: Testing the Singleton
Testing Singleton implementations requires special consideration because the instance persists across tests. However, by using interfaces and dependency injection patterns, we can make Singleton-based code testable. This approach demonstrates how to test Singleton implementations effectively.
Even though it's a Singleton, we can test it using the interface:
[TestFixture]
public class ConfigurationManagerTests
{
[Test]
public void GetSetting_ReturnsValue_WhenSettingExists()
{
// Arrange
Environment.SetEnvironmentVariable("TestKey", "TestValue");
// Act
string value = ConfigurationManager.Instance.GetSetting("TestKey");
// Assert
Assert.AreEqual("TestValue", value);
}
[Test]
public void GetSetting_ReturnsDefault_WhenSettingMissing()
{
// Act
string value = ConfigurationManager.Instance.GetSetting("NonExistentKey", "DefaultValue");
// Assert
Assert.AreEqual("DefaultValue", value);
}
[Test]
public void OrderService_UsesConfiguration_ThroughInterface()
{
// Arrange
var mockConfig = new Mock<IConfigurationManager>();
mockConfig.Setup(c => c.GetSetting("TaxRate", "0.10")).Returns("0.15");
var service = new OrderService(mockConfig.Object);
// Act & Assert
// Test service behavior with mocked configuration
}
}
Real-World Considerations
Implementing Singleton in production applications requires attention to several critical factors beyond the basic pattern implementation. These considerations ensure your Singleton works correctly, handles errors gracefully, and integrates well with the rest of your application architecture.
This example demonstrates several important considerations for real-world Singleton implementations:
1. Thread Safety
The implementation uses Lazy<T> for thread-safe initialization and locking for thread-safe access to the settings dictionary. This ensures the Singleton works correctly in multi-threaded environments.
2. Error Handling
Robust error handling is essential for production Singleton implementations. The implementation handles missing configuration files gracefully without failing initialization, ensuring the application remains functional even when some configuration sources are unavailable.
The implementation handles missing configuration files gracefully without failing initialization. This makes the application more resilient.
3. Configuration Precedence
When loading configuration from multiple sources, establishing a clear precedence order prevents conflicts and ensures predictable behavior. The implementation loads configuration from multiple sources with a clear precedence order:
- Command-line arguments (highest priority)
- Configuration file
- Environment variables (lowest priority)
4. Testability
By implementing an interface, the Singleton can be tested and mocked, addressing one of the common criticisms of the Singleton pattern.
5. Reload Capability
Long-running applications often need to refresh configuration without restarting. The Reload() method allows refreshing configuration without restarting the application, which is useful for long-running applications that need to pick up configuration changes dynamically.
Alternative: Dependency Injection Approach
While Singleton can work for configuration management, modern C# applications often benefit from dependency injection approaches that provide similar single-instance guarantees with better testability and flexibility. For new applications, consider using dependency injection instead:
// Register as singleton in DI container
services.AddSingleton<IConfigurationManager, ConfigurationManager>();
// Inject where needed
public class OrderService
{
private readonly IConfigurationManager _config;
public OrderService(IConfigurationManager config)
{
_config = config; // Injected, testable, flexible
}
}
This provides the same single-instance guarantee with better testability and flexibility.
Related Patterns
Understanding how Singleton relates to other design patterns helps you choose the right pattern for your specific needs and recognize when patterns can be combined effectively. The Big List of Design Patterns provides comprehensive coverage of all design patterns, helping you understand how Singleton relates to other creational patterns like Factory Method and Builder.
Conclusion
Configuration management is one acceptable scenario for Singleton: stateless (or reloadable), shared, and interface-based for testability. Treat it as contextual justification, not a template. For new code, prefer dependency injection with singleton lifetime.
Frequently Asked Questions
Is this Singleton implementation thread-safe?
Yes, this implementation uses Lazy<T> for thread-safe initialization and locking for thread-safe access to the settings dictionary. This ensures the Singleton works correctly in multi-threaded environments.
Can I reload configuration without restarting?
Yes, the Reload() method allows refreshing configuration at runtime. This is useful for long-running applications that need to pick up configuration changes.
How do I test code that uses this Singleton?
Use the InstanceAsInterface property to get the instance as an interface, then inject it into classes that need configuration. This allows mocking in tests while still using the Singleton in production code.
Should I use Singleton or dependency injection for configuration?
For new applications, prefer dependency injection with singleton lifetime. It provides the same single-instance guarantee with better testability and flexibility. However, Singleton can still be appropriate for legacy code or specific use cases.
What happens if configuration loading fails?
The implementation handles errors gracefully. Missing configuration files are skipped, and invalid JSON is logged but doesn't fail initialization. Required settings throw exceptions when accessed if not found.
Can multiple threads access configuration simultaneously?
Yes, the implementation uses locking to ensure thread-safe access. Multiple threads can safely read configuration settings concurrently, and the Reload() method is also thread-safe.

