Introduction
In software architecture, there are scenarios where only a single instance of a class should exist throughout the lifetime of an application. This might be for a logging service, a configuration reader, or a global state manager. The Singleton Pattern is the answer: it ensures a class has only one instance, and it provides a global point of access to that instance.
With C# 14, we can implement Singleton in a more elegant, concise, and thread-safe manner than ever before. In this article, we’ll not only explore the Singleton pattern in depth but also implement a production-grade real-world example: a configuration reader used across many enterprise apps.
๐ What is the Singleton Pattern?
The Singleton Pattern is a creational design pattern that restricts the instantiation of a class to just one object, providing a global point of access to that object.
Common use cases
- Application logging
- Global configuration management
- Caching layers
- Licensing managers
- Connection pools
๐งช When (and When Not) to Use a Singleton
โ
Good Use Cases
- The object holds shared state across the application.
- Creating multiple instances would waste memory or cause conflicts.
- You need lazy loading and thread safety.
โ Avoid If
- The class needs to vary per user/session.
- It holds mutable global state (leads to tight coupling).
- You’re building unit tests—Singletons can make tests harder unless abstracted.
โ๏ธ Implementing Singleton in C# – Patterns Compared
Technique |
Thread-Safe |
Lazy |
Comments |
Basic static instance |
โ |
โ |
Not thread-safe or lazy |
Locking (double-check) |
โ
|
โ
|
Verbose, harder to maintain |
Lazy<T> |
โ
|
โ
|
Clean, idiomatic since .NET 4.0 |
Nested static class |
โ
|
โ
|
Modern, efficient, recommended |
In modern C# (especially from C# 14 onward), the nested static class approach is the cleanest and most performant solution.
๐ง Real-World Singleton: AppConfiguration in C# 14
Let’s now implement a real-world configuration service using the Singleton pattern. This is commonly used to:
- Load application settings (e.g., API endpoints, feature toggles).
- Avoid re-parsing or reading from disk multiple times.
- Provide fast, global access to immutable configuration data.
โ
Class: AppConfiguration.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
public sealed class AppConfiguration
{
private const string ConfigFilePath = "appsettings.json";
private readonly Dictionary<string, string> _settings;
// Singleton accessor
public static AppConfiguration Instance => Nested.instance;
// Private constructor
private AppConfiguration()
{
_settings = LoadSettings();
Console.WriteLine("AppConfiguration loaded at " + DateTime.Now);
}
public string GetSetting(string key, string defaultValue = "") =>
_settings.TryGetValue(key, out var value) ? value : defaultValue;
public void Reload()
{
lock (_settings)
{
var newSettings = LoadSettings();
_settings.Clear();
foreach (var kvp in newSettings)
_settings[kvp.Key] = kvp.Value;
Console.WriteLine("AppConfiguration reloaded at " + DateTime.Now);
}
}
private static Dictionary<string, string> LoadSettings()
{
if (!File.Exists(ConfigFilePath))
throw new FileNotFoundException($"Configuration file not found: {ConfigFilePath}");
var json = File.ReadAllText(ConfigFilePath);
var dictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
return dictionary ?? new Dictionary<string, string>();
}
private class Nested
{
static Nested() { }
internal static readonly AppConfiguration instance = new();
}
}
Example. ๐ appsettings.json
{
"ApiEndpoint": "https://api.example.com",
"MaxItems": "100",
"EnableCaching": "true"
}
๐งช Usage in Program.cs
class Program
{
static void Main()
{
var config = AppConfiguration.Instance;
Console.WriteLine("API Endpoint: " + config.GetSetting("ApiEndpoint"));
Console.WriteLine("Max Items: " + config.GetSetting("MaxItems"));
Console.WriteLine("Caching Enabled: " + config.GetSetting("EnableCaching"));
// Reload the config if external file changes are needed
// config.Reload();
}
}
๐งต Thread Safety and Performance
The Nested class approach
- Is lazy: the AppConfiguration object is created only when the Instance is accessed.
- Is thread-safe: the CLR guarantees that static constructor initialization is thread-safe.
- Avoids locks and verbose double-check patterns.
This ensures maximum performance with minimal complexity—ideal for production code.
๐ง C# 14 Enhancements Utilized
Feature |
Usage |
Init-only behavior |
Immutable dictionary post-init |
Improved readonly use |
Ensures immutability for thread-safety |
Clean record-like design |
Purely functional access with no mutation |
Minimal syntax & nesting |
Simpler class structure for maintainability |
๐ Bonus: Making It Testable
For a testable and decoupled design, abstract behind an interface:
public interface IAppConfiguration
{
string GetSetting(string key, string defaultValue = "");
}
You can now
- Inject IAppConfiguration in classes.
- Mock it in unit tests.
- Still use the Singleton in production.
โ ๏ธ Common Singleton Pitfalls
Pitfall |
Prevention Strategy |
Global mutable state |
Use readonly, immutable collections |
Difficult testing |
Use interfaces and dependency injection |
Hidden dependencies |
Log or document usage; inject where possible |
Threading bugs (older patterns) |
Use CLR’s static constructor behavior |
โ
Conclusion
The Singleton Pattern remains a valuable part of your architecture toolbox when used with care. With C# 14, you can implement it in a clean, robust, and modern way that avoids legacy pitfalls.
๐ Key Takeaways
- Use the nested static class for thread-safe lazy loading.
- Ensure immutability and avoid side effects.
- Add reload or refresh capabilities if needed.
- Consider testability early- abstract with interfaces if necessary.
The AppConfiguration example you’ve seen here is not just academic - this is the kind of architecture used in real production systems, and it scales well across services.