DEV Community

Cover image for Singleton in C#: Stop Writing It Like It's 2005
Vignesh Athiappan
Vignesh Athiappan

Posted on

Singleton in C#: Stop Writing It Like It's 2005

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 */ "";
}
Enter fullscreen mode Exit fullscreen mode

Three things make this bulletproof:

  • sealed — nobody can subclass it and sneak in a second instance.
  • Private constructor — nobody can new it 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>();
Enter fullscreen mode Exit fullscreen mode

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 AddSingleton via DI — same guarantee, fully testable.
  • HttpClient socket 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)