Skip to main content

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...
Thread Safety

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() { }
}
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 SingletonDI 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 stateScoped to the DI container
You manage thread safetyContainer 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 β€” Scoped and Transient DI 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.