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.
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)
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)
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.
// 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 sharedWhen 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
// 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
- Use
Lazy<T>for the simplest thread-safe Singleton - Prefer DI-registered Singletons (
AddSingleton) over static Singletons in modern .NET - A Singleton that holds mutable state must synchronize all access ā
lockorInterlocked - The real problem with Singletons isn't the pattern itself ā it's using them to avoid explicit dependencies
- 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?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.