Singleton Pattern
Definitionβ
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. This is useful when exactly one object is needed to coordinate actions across the system.
Coffee Shop Exampleβ
In a coffee shop, there should be one coffee machine controlling the brewing process β multiple instances would mean inconsistent temperatures and wasted resources.
Naive Implementation (Not Thread-Safe)β
public class CoffeeMachine
{
private static CoffeeMachine? _instance;
public static CoffeeMachine Instance
{
get
{
if (_instance is null)
_instance = new CoffeeMachine();
return _instance;
}
}
private CoffeeMachine()
{
Console.WriteLine("β Coffee machine initialized β heating up...");
}
private int _waterTemperature = 90;
public Coffee BrewEspresso()
{
Console.WriteLine($" Brewing at {_waterTemperature}Β°C...");
return new Espresso();
}
public void SetTemperature(int temp) => _waterTemperature = temp;
}
// Usage
var machine = CoffeeMachine.Instance;
var coffee = machine.BrewEspresso();
// Output: β Coffee machine initialized β heating up...
// Brewing at 90Β°C...
The naive version is not thread-safe. Two threads can simultaneously see _instance as null and create two instances.
Thread-Safe with lockβ
public class CoffeeMachine
{
private static CoffeeMachine? _instance;
private static readonly object _lock = new();
public static CoffeeMachine Instance
{
get
{
lock (_lock)
{
if (_instance is null)
_instance = new CoffeeMachine();
return _instance;
}
}
}
private CoffeeMachine() { }
}
Thread-Safe with Lazy<T> (Recommended)β
public class CoffeeMachine
{
private static readonly Lazy<CoffeeMachine> _instance =
new(() => new CoffeeMachine());
public static CoffeeMachine Instance => _instance.Value;
private CoffeeMachine()
{
Console.WriteLine("β Coffee machine initialized");
}
private int _orderNumber = 0;
public string GenerateOrderId()
{
Interlocked.Increment(ref _orderNumber);
return $"ORD-{_orderNumber:D4}";
}
}
// Usage β thread-safe, lazily initialized
var order1 = CoffeeMachine.Instance.GenerateOrderId(); // ORD-0001
var order2 = CoffeeMachine.Instance.GenerateOrderId(); // ORD-0002
.NET-Specific: DI-Managed Singletonβ
In ASP.NET Core, prefer DI-managed singletons over the classic pattern. The DI container handles thread safety and makes dependencies explicit.
// Service registration
builder.Services.AddSingleton<CoffeeMachine>();
// Constructor injection β dependencies are visible and testable
public class OrderService
{
private readonly CoffeeMachine _coffeeMachine;
public OrderService(CoffeeMachine coffeeMachine)
{
_coffeeMachine = coffeeMachine;
}
public string CreateOrder()
{
var orderId = _coffeeMachine.GenerateOrderId();
return orderId;
}
}
Why DI Singleton Is Betterβ
| Classic Singleton | DI Singleton |
|---|---|
Hidden dependency (CoffeeMachine.Instance everywhere) | Explicit dependency (constructor injection) |
| Hard to unit test (can't swap the instance) | Easy to mock or replace in tests |
| Global mutable state | Scoped to the DI container |
| You manage thread safety | Container manages thread safety |
When to Useβ
- Shared configuration or settings
- Connection pool or resource manager
- Logging service
- Caching layer
When NOT to Useβ
- As a "global variable" to pass data between classes β use proper DI instead
- When you need multiple instances in different scopes β use Scoped lifetime in DI
- When the instance holds mutable state that tests need to reset β use DI and recreate per test
Key Takeawaysβ
- Use
Lazy<T>for thread-safe lazy initialization if you must implement the pattern manually - In ASP.NET Core, always prefer
services.AddSingleton<T>()over the classic pattern - The Singleton pattern restricts to one instance β
ScopedandTransientDI lifetimes serve different needs - A private constructor prevents external instantiation
Interview Questionsβ
Q: Is Singleton thread-safe by default?
No. You need lock, Lazy<T>, or a static initializer. Lazy<T> is the cleanest approach.
Q: How is Singleton different from a static class? A static class can't implement interfaces, can't be passed as a parameter, and can't be lazy-initialized. A singleton is a real object with a single instance.
Q: Why is Singleton considered an anti-pattern? It introduces hidden global state and tight coupling, making testing difficult. In modern .NET, DI-managed singletons solve the same problem transparently.
Related Topicsβ
- Dependency Injection β DI-managed singletons
- Factory Method β another way to control object creation
- SOLID β DIP β depend on abstractions, not concrete singletons