.NET  

Singleton Pattern in .NET

The Singleton pattern is a creational design pattern that ensures a class has only one instance throughout the application’s lifetime and that that instance is available for access from anywhere.

While building the software, we often encounter scenarios where multiple instances of a class could lead to problems like inconsistent state/data, resource conflicts, or inefficient resource utilization. Singleton pattern solves this by ensuring a class has only one instance, and that will be used globally accross a program.

Prerequisites

  • Visual Studio 2022 installed,

  • Basics of .NET Core

Source Code

The source code is publicly available on github: Link

When Should We Use the Singleton Pattern?

Logging Systems

In most applications, logging should be handled by a single logger instance.

Using a singleton logger ensures:

  • All log messages go to the same place (file, database, or monitoring system)

  • Logs are written in a consistent format

  • No conflicts occur due to multiple logger instances

This makes debugging and monitoring much easier.

Configuration Manager

Application configuration such as connection strings, API keys, or feature flags should be loaded once and shared across the application.

A singleton configuration manager:

  • Loads settings a single time

  • Provides fast, global access to configuration values

  • Ensures consistency across different modules

This avoids repeated file or database reads and improves performance.

Database Connections

Using a singleton for managing database access will:

  • Prevents creating multiple unnecessary connections

  • Helps control connection usage

  • Centralizes database-related logic

Thread Pools

Thread pools are shared resources that should be managed centrally.

Using a singleton thread pool will:

  • Avoids uncontrolled thread creation

  • Ensures efficient use of system resources

  • Improves application stability and performance

A single and well-managed thread pool is better than multiple competing each others.

When we should avoid Singleton

Singleton should not be used when:

  • You need multiple instances with different states

  • The class holds user-specific or request-specific data

  • You want easy unit testing without tight coupling

In many modern .NET Core applications, Dependency Injection (DI) can be a better alternative.

Building blocks of the Singleton Design Pattern

  • Private Parameterless constructor: It prevents creating an instance using new keyword from outside the class.

  • Sealed Class: It prevents inheritance and also avoids the polymerphism and overridden behavior.

  • Private Static Variable: It holds the single instance of the class and having it private ensures the controlled access as well.

  • Public Static Method or Property: This will provide central access point to the instance so that we can ensure only one instance existed.

Practical ways to implement the Singleton Design Pattern in C#

Below are the most common and practical ways to implement the Singleton Design Pattern.

  1. Simple Singleton (NOT Thread-Safe)

  2. Thread-Safe Singleton using Lock

  3. Double-Checked Locking Singleton

  4. Eager Initialization Singleton

  5. Lazy Initialization Singleton

  6. Static Class based Singleton

  7. Singleton via Dependency Injection

1. Simple Singleton (NOT Thread-Safe)

This version of the Singleton pattern is very easy to understand, making it ideal for learning and demonstrations. Its simple structure clearly shows how a single instance is created and reused, and it works correctly in single-threaded applications such as basic console programs.

However, it is not thread-safe. In multi-threaded environments, multiple threads may create more than one instance at the same time, which breaks the Singleton rule. Because of this, it should be used only for learning purposes or simple single-threaded tools, and not in production systems.

Add a AppConfigSingleton.cs file

  
    namespace SingletonPatternInCSharp.Simple
{
    /// <summary>
    /// Basic singleton implementation - NOT thread-safe!
    /// Demonstrates the simplest form of singleton pattern.
    /// </summary>
    public class AppConfigSingleton
    {
        private static AppConfigSingleton? _instance;

        // Private constructor
        private AppConfigSingleton()
        {
            ApplicationName = "Singleton Demo App";
            Version = "1.0.0";
        }

        public static AppConfigSingleton Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new AppConfigSingleton();
                }

                return _instance;
            }
        }

        public string ApplicationName { get; }
        public string Version { get; }
    }
}
  

Now access it from program.cs as

  
    using SingletonPatternInCSharp.Simple;

var config1 = AppConfigSingleton.Instance;
var config2 = AppConfigSingleton.Instance;

Console.WriteLine($"App: {config1.ApplicationName}");
Console.WriteLine($"Version: {config1.Version}");

// Both references point to the same instance
Console.WriteLine(ReferenceEquals(config1, config2)); // True
  

2. Thread-Safe Singleton using Lock

This ensures that only one instance of the class is created, even when multiple threads try to access it at the same time. This approach is thread-safe and straightforward to implement, making it suitable for scenarios where multiple threads might try to create the instance simultaneously.

However, there is a performance cost because the lock is applied every time the instance is accessed, even after the Singleton has been created. Despite this, it works well for small-scale multi-threaded applications where the performance overhead is negligible, providing a simple and reliable way to maintain a single shared instance.

Add a LoggerSingleton.cs file

  
    namespace SingletonPatternInCSharp.Thread_Safe_using_Lock
{
    public class LoggerSingleton
    {
        private static LoggerSingleton? _instance;
        private static readonly object _lock = new object();

        // Private constructor prevents external instantiation
        private LoggerSingleton() { }

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

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

Now access it from program.cs as

// Simulate multiple threads logging at the same time
Parallel.For(0, 5, i =>
{
    var logger = LoggerSingleton.Instance;
    logger.Log($"Message from task {i}");
});

// Verify single instance
var logger1 = LoggerSingleton.Instance;
var logger2 = LoggerSingleton.Instance;
Console.WriteLine(ReferenceEquals(logger1, logger2)); // True

3. Double-Checked Locking Singleton

This is a thread-safe implementation that minimizes the performance overhead of locking. It checks whether the instance already exists before acquiring the lock, and again inside the lock if necessary. This ensures that the lock is applied only once, when the Singleton is first created, making it highly efficient for systems where the instance is accessed frequently.

However, this approach is slightly more complex than a simple locked Singleton and must be implemented carefully to avoid subtle threading issues. It is best suited for high-performance systems where a Singleton is accessed often, providing a balance between thread safety and runtime efficiency.

Add DatabaseManagerSingleton.cs

namespace SingletonPatternInCSharp.Double_Checked_Locking
{
    public class DatabaseManagerSingleton
    {
        private static DatabaseManagerSingleton? _instance;
        private static readonly object _lock = new object();

        // Private constructor prevents external instantiation
        private DatabaseManagerSingleton()
        {
            // Simulate opening a database connection
            Console.WriteLine("Database connection initialized.");
        }

        public static DatabaseManagerSingleton Instance
        {
            get
            {
                // First check without locking (performance optimization)
                if (_instance == null)
                {
                    lock (_lock)
                    {
                        // Double-check inside the lock
                        if (_instance == null)
                        {
                            _instance = new DatabaseManagerSingleton();
                        }
                    }
                }

                return _instance;
            }
        }

        // Example method to simulate database query
        public void ExecuteQuery(string query)
        {
            Console.WriteLine($"Executing query: {query}");
        }
    }
}

Now access it from program.cs as:

// Simulate multiple threads accessing the database manager
Parallel.For(0, 5, i =>
{
    var dbManager = DatabaseManagerSingleton.Instance;
    dbManager.ExecuteQuery($"SELECT * FROM Users WHERE Id = {i}");
});

// Verify single instance
var db1 = DatabaseManagerSingleton.Instance;
var db2 = DatabaseManagerSingleton.Instance;
Console.WriteLine(ReferenceEquals(db1, db2)); // True

4. Eager Initialization Singleton

The Eager Initialization Singleton creates the single instance of the class as soon as the application starts, rather than waiting until it is first needed. This approach is thread-safe by default because the instance is created before any threads can access it, and the implementation is very simple and clean, making it easy to understand and use.

The main drawback is that the instance is created even if it is never used, which could be wasteful for heavy objects. This pattern is best suited for situations where the Singleton is lightweight and the application always requires the instance, ensuring it is available immediately when needed.

Add AppSettingsSingleton.cs

namespace SingletonPatternInCSharp.Eager_Initialization
{
    public class AppSettingsSingleton
    {
        // Eagerly created instance (thread-safe by default)
        private static readonly AppSettingsSingleton _instance = new AppSettingsSingleton();

        // Private constructor prevents external instantiation
        private AppSettingsSingleton()
        {
            AppName = "Eager Singleton Demo App";
            Version = "1.0.0";
            MaxUsers = 100;
        }

        public static AppSettingsSingleton Instance => _instance;

        // Example configuration properties
        public string AppName { get; }
        public string Version { get; }
        public int MaxUsers { get; }
    }
}

Now, access it from program.cs

var settings1 = AppSettingsSingleton.Instance;
var settings2 = AppSettingsSingleton.Instance;

Console.WriteLine($"App: {settings1.AppName}");
Console.WriteLine($"Version: {settings1.Version}");
Console.WriteLine($"Max Users: {settings1.MaxUsers}");

// Verify both references point to the same instance
Console.WriteLine(ReferenceEquals(settings1, settings2)); // True

5. Lazy Initialization Singleton

The Lazy Initialization Singleton uses the Lazy<T> type to create the instance only when it is first accessed, rather than at application startup. This approach is thread-safe by default, easy to read, and avoids the complexity of manual locking. The internal implementation of Lazy<T> handles synchronization and edge cases, making the code clean, reliable, and less error-prone.

There is a very small performance overhead due to the use of Lazy<T>, but in practice this cost is negligible. Because of its safety, clarity, and maintainability, this pattern is recommended for most production systems, including ASP.NET Core applications, microservices, and enterprise-level software.

Add CacheManagerSingleton.cs

namespace SingletonPatternInCSharp.Lazy_Initialization
{
    public class CacheManagerSingleton
    {
        private static readonly Lazy<CacheManagerSingleton> _instance =
            new Lazy<CacheManagerSingleton>(() => new CacheManagerSingleton());

        // Private constructor
        private CacheManagerSingleton()
        {
            Console.WriteLine("Cache Manager initialized.");
            _cache = new Dictionary<string, string>();
        }

        public static CacheManagerSingleton Instance => _instance.Value;

        private readonly Dictionary<string, string> _cache;

        public void Add(string key, string value)
        {
            _cache[key] = value;
        }

        public string Get(string key)
        {
            return _cache.TryGetValue(key, out var value)
                ? value
                : "Not Found";
        }
    }
}

Access it from program.cs as:

// Cache is NOT created yet
Parallel.For(0, 3, i =>
{
    var cache = CacheManagerSingleton.Instance;
    cache.Add($"key{i}", $"value{i}");
});

var cacheManager = CacheManagerSingleton.Instance;
Console.WriteLine(cacheManager.Get("key1"));

6. Static Class based Singleton

A static class is often used as a Singleton alternative in C#, even though it is not a true implementation of the Singleton pattern. Since static members are initialized by the runtime and exist only once per application domain, this approach is thread-safe by default, has no instantiation issues, and offers very fast access without any additional overhead.

However, static classes come with important limitations. They cannot implement interfaces, do not support inheritance, and offer no control over lazy initialization beyond what the runtime provides. Because of these constraints, static classes are best suited for utility-style functionality, such as configuration values, constants, helper methods, and shared utilities, where simplicity and performance are more important than flexibility or extensibility.

Add DatabaseConnectionProvider.cs

using Microsoft.Data.SqlClient;

namespace SingletonPatternInCSharp.Static_Class
{
    public static class DatabaseConnectionProvider
    {
        private static readonly string _connectionString =
            "Server=localhost;Database=AppDb;Trusted_Connection=True;";

        public static SqlConnection GetConnection()
        {
            return new SqlConnection(_connectionString);
        }
    }
}

Access it from program.cs as:

SqlConnection connection1 = DatabaseConnectionProvider.GetConnection();
SqlConnection connection2 = DatabaseConnectionProvider.GetConnection();

Console.WriteLine(connection1.ConnectionString);
Console.WriteLine(connection2.ConnectionString);

// These are different objects, created by a static provider
Console.WriteLine(ReferenceEquals(connection1, connection2)); // False

7. Singleton via Dependency Injection

The Singleton via Dependency Injection approach relies on the application’s DI container to manage the lifetime of the object instead of implementing the Singleton pattern manually. By registering a service as a singleton, the framework ensures that only one instance is created and shared throughout the application. This results in cleaner code, with no explicit singleton logic, locks, or static access.

This approach is highly testable, as services can be easily mocked or replaced, and the object’s lifecycle is fully managed by the framework. The main drawback is that it requires a DI container, which may not be suitable for very simple applications. It is the preferred approach in ASP.NET Core and modern .NET applications, where maintainability, scalability, and clean architecture are key design goals.

Add ILoggerService.cs

namespace SingletonPatternInCSharp.Dependency_Injection
{
    public interface ILoggerService
    {
        void Log(string message);
    }
}

Add LoggerService.cs

namespace SingletonPatternInCSharp.Dependency_Injection
{
    public class LoggerService : ILoggerService
    {
        public void Log(string message)
        {
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}");
        }
    }
}

Access it from program.cs as:

// Create service collection
var services = new ServiceCollection();

// Register LoggerService as Singleton
services.AddSingleton<ILoggerService, LoggerService>();

// Build service provider
var serviceProvider = services.BuildServiceProvider();

// Resolve service multiple times
var singletonLogger1 = serviceProvider.GetRequiredService<ILoggerService>();
var singletonLogger2 = serviceProvider.GetRequiredService<ILoggerService>();

singletonLogger1.Log("First log message");
singletonLogger2.Log("Second log message");

// Verify singleton behavior
Console.WriteLine(ReferenceEquals(singletonLogger1, singletonLogger2)); // True

Cheers !