Singleton is the first design pattern everyone learns and the first one everyone writes wrong. Here's the modern way, the trap that catches people in interviews, and why in real .NET projects you probably shouldn't hand-roll it at all.
What Singleton actually promises
One instance. One global access point. That's it.
You reach for it when something is expensive to create or must hold shared state — configuration managers, connection pools, caches, logging.
The naive version (and why it breaks)
public class ConfigManager
{
private static ConfigManager _instance;
public static ConfigManager Instance
{
get
{
if (_instance == null) // Thread A and B both pass this
_instance = new ConfigManager(); // Two instances created
return _instance;
}
}
}
Under concurrent load, two threads can pass the null check simultaneously and you end up with two instances. Your "singleton" silently isn't one. The classic fix was double-check locking with a lock block — verbose, easy to get subtly wrong.
The modern version: Lazy<T>
public sealed class ConfigManager
{
private static readonly Lazy<ConfigManager> _instance =
new(() => new ConfigManager());
public static ConfigManager Instance => _instance.Value;
private ConfigManager()
{
// load configuration once
}
public string GetEndpoint(string apiName) => /* lookup */ "";
}
Three things make this bulletproof:
-
sealed— nobody can subclass it and sneak in a second instance. -
Private constructor — nobody can
newit from outside. -
Lazy<T>— thread-safe by default and created only on first access. All the double-check locking boilerplate disappears.
The plot twist: you probably shouldn't write this
In modern .NET, the DI container gives you the same guarantee with one line:
builder.Services.AddSingleton<IConfigManager, ConfigManager>();
Same single-instance lifetime, but now it's behind an interface — which means you can mock it in unit tests. Static singletons are the opposite: they hide dependencies and make tests miserable. That hidden-dependency problem is the main reason Singleton gets called an anti-pattern.
Rule of thumb: understand the hand-rolled version for interviews; use DI-registered singletons in production code.
The HttpClient example everyone should know
Ever wondered why new HttpClient() per request is a famous .NET mistake? Each instance opens its own connection pool, and disposed instances leave sockets in TIME_WAIT. Under load, you exhaust ports — socket exhaustion. The fix is treating HttpClient as a long-lived shared instance, or better, using IHttpClientFactory, which pools and recycles handlers for you. It's the Singleton idea applied to a real operational problem.
TL;DR
- Singleton = one instance + global access.
- Use
Lazy<T>if you must hand-roll it:sealed+ private constructor +Lazy<T>. - In real projects, prefer
AddSingletonvia DI — same guarantee, fully testable. -
HttpClientsocket exhaustion is the canonical real-world case for singleton-style reuse.
This is part 1 of a 15-part series on must-know design patterns in C#. Next up: Factory Method.
Top comments (0)