The Singleton pattern ensures that a class has exactly one instance throughout the lifetime of an application and provides a global point of access to that instance. This guide covers multiple implementation approaches in C# .NET, from the simplest to the most robust, along with guidance on when singletons are appropriate and when you should reach for alternatives.
When to Use a Singleton
Singletons are useful when:
- You need exactly one instance of a class to coordinate actions across the system (for example, a configuration manager, logging service, or connection pool).
- Creating multiple instances would cause conflicts or waste resources.
- The instance must be accessible globally without passing it through every method call.
Implementation 1: Basic (Not Thread-Safe)
This is the simplest implementation, but it is not safe for multithreaded applications:
public sealed class Singleton
{
private static Singleton _instance;
private Singleton() { }
public static Singleton Instance
{
get
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
Problem: If two threads check _instance == null at the same time, both will create an instance, violating the singleton guarantee.
Implementation 2: Thread-Safe with Lock
Adding a lock statement prevents multiple threads from creating the instance simultaneously:
public sealed class Singleton
{
private static Singleton _instance;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
}
This is thread-safe, but every call to Instance acquires the lock, which introduces a performance cost even after the instance has been created.
Implementation 3: Double-Check Locking
Double-check locking reduces the overhead by only acquiring the lock when the instance has not yet been created:
public sealed class Singleton
{
private static volatile Singleton _instance;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}
}
The volatile keyword ensures that the assignment to _instance is completed before the variable is accessed by other threads. The outer null check avoids the lock once the instance exists. The inner null check handles the race condition between two threads that both passed the outer check.
Implementation 4: Static Initialization (Eager)
The .NET runtime guarantees that a static constructor runs exactly once per application domain, making this approach inherently thread-safe:
public sealed class Singleton
{
private static readonly Singleton _instance = new Singleton();
// Explicit static constructor tells the runtime
// not to mark the type with beforefieldinit
static Singleton() { }
private Singleton() { }
public static Singleton Instance => _instance;
}
The instance is created when the class is first referenced. The explicit static constructor prevents the runtime from using the beforefieldinit optimization, which could cause the instance to be created earlier than expected.
Trade-off: The instance is created as soon as any static member is accessed, even if Instance is never called. For most applications, this is not a problem.
Implementation 5: Lazy (Recommended)
The Lazy<T> class, introduced in .NET 4, provides the cleanest thread-safe lazy initialization:
public sealed class Singleton
{
private static readonly Lazy<Singleton> _lazy =
new Lazy<Singleton>(() => new Singleton());
private Singleton() { }
public static Singleton Instance => _lazy.Value;
}
Lazy<T> handles all the thread synchronization internally. The instance is not created until the first time Instance is accessed. This is the recommended approach for most C# applications because it is concise, correct, and easy to understand.
You can also control the thread-safety mode:
// Default: thread-safe (LazyThreadSafetyMode.ExecutionAndPublication)
new Lazy<Singleton>(() => new Singleton());
// Not thread-safe (use only if single-threaded)
new Lazy<Singleton>(() => new Singleton(), isThreadSafe: false);
// Thread-safe, but each thread may create its own instance; only one is kept
new Lazy<Singleton>(() => new Singleton(),
LazyThreadSafetyMode.PublicationOnly);
Making the Singleton Class Sealed
All examples above use the sealed keyword. This prevents subclasses from creating additional instances through inheritance. Without sealed, a derived class could expose its own constructor and break the single-instance guarantee.
Adding Functionality
A singleton can hold state and provide methods just like any other class:
public sealed class AppSettings
{
private static readonly Lazy<AppSettings> _lazy =
new Lazy<AppSettings>(() => new AppSettings());
public static AppSettings Instance => _lazy.Value;
public string ConnectionString { get; private set; }
public int MaxRetries { get; private set; }
private AppSettings()
{
// Load settings from configuration file
ConnectionString = "Server=.;Database=MyApp;Trusted_Connection=true;";
MaxRetries = 3;
}
public void Reload()
{
// Re-read settings from configuration file
}
}
Usage:
var connStr = AppSettings.Instance.ConnectionString;
Why You Should Consider Alternatives
While singletons solve real problems, they come with well-known drawbacks:
- Hidden dependencies. Code that calls
Singleton.Instancehas an implicit dependency that is not visible in its constructor signature. - Difficult to test. You cannot easily substitute a mock or fake implementation in unit tests.
- Global mutable state. If the singleton holds mutable state, it can introduce hard-to-debug issues in concurrent applications.
- Lifetime management. The singleton lives for the entire application lifetime, which may not be appropriate for all resources.
Preferred Alternative: Dependency Injection
Modern .NET applications typically use a dependency injection (DI) container to manage object lifetimes. You can register any class with a singleton lifetime:
// In Program.cs or Startup.cs
builder.Services.AddSingleton<IAppSettings, AppSettings>();
Then inject it through the constructor:
public class OrderService
{
private readonly IAppSettings _settings;
public OrderService(IAppSettings settings)
{
_settings = settings;
}
}
This approach gives you the same single-instance behavior while keeping dependencies explicit, supporting interface-based design, and enabling easy unit testing with mocks.
Comparison Summary
| Approach | Thread-Safe | Lazy | Complexity | Recommended |
|---|---|---|---|---|
| Basic (no lock) | No | Yes | Low | No |
| Simple lock | Yes | Yes | Low | For simple cases |
| Double-check locking | Yes | Yes | Medium | Legacy code |
| Static initialization | Yes | No | Low | When eagerness is fine |
| Lazy<T> | Yes | Yes | Low | Yes |
| DI container | Yes | Configurable | Low | Best for modern apps |
Summary
For most C# .NET applications, the Lazy<T> approach is the best standalone singleton implementation: it is thread-safe, lazy, and easy to read. However, in modern applications using dependency injection, prefer registering your service with AddSingleton<T>() in the DI container. This gives you the same single-instance guarantee with the added benefits of explicit dependencies, testability, and cleaner architecture.