.NET & C# Development · Lesson 30 of 229
Singleton — One Instance, Controlled Access
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 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
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
- 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