πŸ“„

Singleton Pattern

Intermediate 2 min read 300 words

Ensure a class has only one instance and provide global access to it

Singleton Pattern

Intent

Ensure a class has only one instance and provide a global point of access to it.

Problem

You need exactly one instance of a class to:

  • Coordinate actions across the system (logging, configuration)
  • Manage shared resources (connection pools, caches)
  • Provide centralized control (settings, state)

Solution

Modern C# - Lazy (Recommended)

public sealed class DatabaseConnection
{
    private static readonly Lazy<DatabaseConnection> _instance =
        new Lazy<DatabaseConnection>(() => new DatabaseConnection());

    public static DatabaseConnection Instance => _instance.Value;

    private readonly string _connectionString;

    private DatabaseConnection()
    {
        Console.WriteLine("Initializing database connection...");
        _connectionString = "Server=localhost;Database=myapp;";
        // Expensive initialization only happens once
    }

    public void Query(string sql)
    {
        Console.WriteLine($"Executing: {sql}");
    }
}

// Usage
var db = DatabaseConnection.Instance;
db.Query("SELECT * FROM Users");

var db2 = DatabaseConnection.Instance;
Console.WriteLine(db == db2); // True - same instance

Static Constructor (Also Thread-Safe)

public sealed class Logger
{
    private static readonly Logger _instance = new Logger();

    // Static constructor guarantees single initialization
    static Logger() { }

    public static Logger Instance => _instance;

    private Logger()
    {
        Console.WriteLine("Logger initialized");
    }

    public void Log(string message)
    {
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}");
    }
}

Generic Singleton Base

public abstract class Singleton<T> where T : class
{
    private static readonly Lazy<T> _instance =
        new Lazy<T>(() => CreateInstance());

    public static T Instance => _instance.Value;

    private static T CreateInstance()
    {
        // Find non-public constructor
        var constructor = typeof(T).GetConstructor(
            BindingFlags.Instance | BindingFlags.NonPublic,
            null, Type.EmptyTypes, null);

        if (constructor == null)
            throw new InvalidOperationException(
                $"Type {typeof(T)} must have a private parameterless constructor");

        return (T)constructor.Invoke(null);
    }
}

// Usage
public sealed class AppConfig : Singleton<AppConfig>
{
    public string AppName { get; } = "MyApplication";
    public string Version { get; } = "1.0.0";

    private AppConfig() { } // Private constructor required
}

var config = AppConfig.Instance;
Console.WriteLine(config.AppName);

Anti-Patterns to Avoid

Double-Checked Locking (Outdated)

// AVOID: Complex and error-prone
public sealed class OldStyleSingleton
{
    private static OldStyleSingleton _instance;
    private static readonly object _lock = new object();

    public static OldStyleSingleton Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (_lock)
                {
                    if (_instance == null)
                    {
                        _instance = new OldStyleSingleton();
                    }
                }
            }
            return _instance;
        }
    }
}

// BETTER: Use Lazy<T> instead

Non-Thread-Safe (Bug)

// BAD: Race condition possible
public sealed class BrokenSingleton
{
    private static BrokenSingleton _instance;

    public static BrokenSingleton Instance
    {
        get
        {
            // Two threads could both create instances!
            if (_instance == null)
            {
                _instance = new BrokenSingleton();
            }
            return _instance;
        }
    }
}

Singleton vs Static Class

Aspect Singleton Static Class
Inheritance Can implement interfaces Cannot
Instance control Lazy initialization Always available
Testing Can be mocked (with interface) Hard to mock
State Instance state Static state only
Dependency injection Supported Not directly

Testable Singleton

// Interface for testability
public interface IApplicationSettings
{
    string GetSetting(string key);
    void SetSetting(string key, string value);
}

// Singleton implementation
public sealed class ApplicationSettings : IApplicationSettings
{
    private static readonly Lazy<ApplicationSettings> _instance =
        new Lazy<ApplicationSettings>(() => new ApplicationSettings());

    public static ApplicationSettings Instance => _instance.Value;

    private readonly Dictionary<string, string> _settings = new();

    private ApplicationSettings()
    {
        // Load settings from file/database
        _settings["Environment"] = "Production";
    }

    public string GetSetting(string key) =>
        _settings.TryGetValue(key, out var value) ? value : null;

    public void SetSetting(string key, string value) =>
        _settings[key] = value;
}

// In production
services.AddSingleton<IApplicationSettings>(ApplicationSettings.Instance);

// In tests
services.AddSingleton<IApplicationSettings>(new MockSettings());

Singleton with Dependency Injection

Modern preference: Let the DI container manage singleton lifetime.

// Service that should be singleton
public class CacheService : ICacheService
{
    private readonly ConcurrentDictionary<string, object> _cache = new();

    public void Set(string key, object value) => _cache[key] = value;
    public T Get<T>(string key) => _cache.TryGetValue(key, out var v) ? (T)v : default;
}

// Register as singleton in DI
services.AddSingleton<ICacheService, CacheService>();

// Framework ensures single instance
public class UserService
{
    private readonly ICacheService _cache;

    public UserService(ICacheService cache) // Same instance every time
    {
        _cache = cache;
    }
}

When to Use Singleton

Good Use Cases:

  • Configuration - Read-only settings
  • Logging - Centralized log management
  • Caching - Shared cache instance
  • Connection pooling - Database connection management

Avoid Singleton When:

  • State changes frequently
  • Testing requires different instances
  • Object has heavy dependencies
  • Simple static methods would suffice

Real-World Examples

// HttpClient - Microsoft recommends singleton usage
public class ApiClientService
{
    private static readonly HttpClient _httpClient = new HttpClient
    {
        BaseAddress = new Uri("https://api.example.com"),
        Timeout = TimeSpan.FromSeconds(30)
    };

    public async Task<string> GetAsync(string path)
    {
        return await _httpClient.GetStringAsync(path);
    }
}

// Configuration
public sealed class AppConfiguration
{
    private static readonly Lazy<AppConfiguration> _instance =
        new Lazy<AppConfiguration>(() => new AppConfiguration());

    public static AppConfiguration Instance => _instance.Value;

    public string DatabaseConnection { get; }
    public string ApiKey { get; }
    public bool IsProduction { get; }

    private AppConfiguration()
    {
        var config = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .Build();

        DatabaseConnection = config["ConnectionStrings:Default"];
        ApiKey = config["ApiKey"];
        IsProduction = config["Environment"] == "Production";
    }
}

Interview Tips

Common Questions:

  • β€œHow do you implement thread-safe singleton?”
  • β€œWhat are the drawbacks of singleton?”
  • β€œWhen would you use singleton vs static class?”

Key Points:

  1. Use Lazy<T> for thread-safe lazy initialization
  2. Prefer DI container for singleton management in modern apps
  3. Singletons can make testing difficult
  4. Hidden dependencies (global state) are a code smell
  5. .NET HttpClient is a good example of recommended singleton usage

Red Flags:

  • Singletons everywhere (overuse)
  • Using singleton to avoid proper DI
  • Mutable singleton state without synchronization
  • Static classes disguised as singletons