Back to blog
Backend Systemsbeginner

Singleton Pattern — When to Use It and When Not To

Learn the Singleton design pattern in C#: thread-safe implementation with Lazy<T>, when Singleton is appropriate, and why overusing it leads to hidden coupling.

Asma HafeezApril 17, 20263 min read
csharpdesign-patternssingletondotnet
Share:š•

Singleton Pattern

Singleton ensures a class has exactly one instance and provides global access to it. It's the most recognized pattern — and the most misused.


The Problem It Solves

Some resources should be shared across an application:

  • Database connection pool (one pool, many connections)
  • Configuration store (loaded once)
  • Logger factory (one factory, many loggers)
  • In-memory cache

Creating multiple instances would waste resources or cause bugs.


Classic Implementation (Thread-Safe with Lazy)

C#
public class AppConfig
{
    // Lazy<T> is thread-safe by default
    private static readonly Lazy<AppConfig> _instance =
        new(() => new AppConfig());

    public static AppConfig Instance => _instance.Value;

    // Private constructor — prevents external instantiation
    private AppConfig()
    {
        // Load configuration once
        ConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION")
            ?? "Server=localhost;Database=myapp";
        MaxConnections = 20;
    }

    public string ConnectionString { get; }
    public int MaxConnections { get; }
}

// Usage
var connStr = AppConfig.Instance.ConnectionString;

Thread-Safe Without Lazy (static field)

C#
public class Counter
{
    // Static fields are initialized once per AppDomain — inherently thread-safe for initialization
    private static readonly Counter _instance = new();
    public static Counter Instance => _instance;

    private int _count = 0;
    private readonly object _lock = new();

    private Counter() { }

    // Thread-safe increment
    public int Increment()
    {
        lock (_lock)
        {
            return ++_count;
        }
    }

    // Interlocked is faster for simple int operations
    private int _atomicCount = 0;
    public int AtomicIncrement() => Interlocked.Increment(ref _atomicCount);
}

Singleton in .NET DI

In ASP.NET Core, the right way to get Singleton behavior is via DI — not static classes.

C#
// Register as Singleton — one instance per application lifetime
builder.Services.AddSingleton<IConnectionPool, ConnectionPool>();
builder.Services.AddSingleton<ICacheProvider, MemoryCacheProvider>();

// vs Scoped (one per request) and Transient (new every time)
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();

This is better than the classic Singleton pattern because:

  • The class doesn't know it's a singleton — the DI container decides
  • Easy to test — swap the singleton with a mock in tests
  • Properly disposed at app shutdown

When Singleton Is Appropriate

āœ“ Shared, stateless infrastructure (connection pools, HTTP clients)
āœ“ Configuration that's read-only after startup
āœ“ Logger factories
āœ“ In-memory caches (with thread-safe access)
āœ“ Expensive resources that should be shared

When NOT to Use Singleton

āœ— Mutable state that's user-specific (user session, shopping cart)
āœ— When you need one-per-request isolation (DB transactions)
āœ— As a global service locator — call GetService() everywhere
āœ— To avoid passing dependencies — just take the dependency in the constructor

The Hidden Coupling Problem

C#
// BAD — hidden dependency on a global singleton
public class OrderService
{
    public void PlaceOrder(Order order)
    {
        var db = DatabaseSingleton.Instance;  // hidden dependency!
        db.Save(order);
        // Hard to test, hard to change, couples OrderService to the singleton
    }
}

// GOOD — explicit dependency injection
public class OrderService(IDatabase db)
{
    public void PlaceOrder(Order order)
    {
        db.Save(order);  // explicit dependency — easy to test, easy to swap
    }
}

Register IDatabase as Singleton in DI if one instance is needed — but inject it explicitly.


Key Takeaways

  1. Use Lazy<T> for the simplest thread-safe Singleton
  2. Prefer DI-registered Singletons (AddSingleton) over static Singletons in modern .NET
  3. A Singleton that holds mutable state must synchronize all access — lock or Interlocked
  4. The real problem with Singletons isn't the pattern itself — it's using them to avoid explicit dependencies
  5. Ask: "Should this be a Singleton, or should I inject it as one?" — DI-registered services are testable; static singletons are not

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.