LogIn
I don't have account.

Mastering the Singleton Design Pattern in C#

Code Crafter
160 Views

In large-scale software systems, it is often necessary to ensure that a particular class has only one instance throughout the lifecycle of an application. Whether it’s managing global configuration, logging or a database connection pool, allowing multiple instances could lead to inconsistent states and race conditions.

The Singleton Design Pattern addresses this by ensuring that a class has only one instance and provides a global point of access to it.

✅ “Ensure a class has only one instance and provide a global point of access to it.” — Gang of Four

In this advanced guide, we’ll explore the Singleton Pattern in-depth, walk through real-world examples, analyze different implementation strategies (including thread safety), highlight pros and cons, discuss anti-pattern traps, and explore modern C# best practices.

What is Singleton Pattern?

The Singleton Pattern is one of the simplest and most widely used. It ensures that a class has only one instance throughout the application and provides a global point of access to it.

Real-World Analogy: President of a Country

Think of the President of a country. Regardless of how many people or systems reference "the President", there is only one official instance at any time. Multiple instances could result in conflicting commands or chaos.

Similarly, the Singleton pattern ensures one authoritative instance exists within your codebase.

Structure of the Singleton Design Pattern

The Singleton Method Design Pattern typically involves the following components :

  • Private Constructor : Prevents external instantiation via new
  • Static Instance Field : Holds the reference to the single created instance
  • Public Static Accessor : Returns the singleton instance (Instance or GetInstance() method)
public sealed class Singleton
{
    // Static variable to hold the single instance
    private static readonly Lazy<Singleton> _instance =
        new Lazy<Singleton>(() => new Singleton());

    // Public property to access the instance
    public static Singleton Instance => _instance.Value;

    // Private constructor to prevent external instantiation
    private Singleton() { }

    // Optional: other business logic
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

Implementation of Singleton design pattern

1. Eager Initialization Singleton

Creates the Singleton instance at class loading time, regardless of whether it's used.

public sealed class Singleton
{
    private static readonly Singleton _instance = new Singleton();
    public static Singleton Instance => _instance;
    private Singleton() { }
}
  • Pros : Simple , Thread-safe by CLR (static initialization is atomic)
  • Cons : No lazy loading, Instance created even if never used
  • Use When : Instance is lightweight, Always needed (e.g., config manager)

2. Basic Implementation | Lazy Initialization (Not Thread-Safe)

Instance is created only when requested for the first time - but no locking is used.

public class Singleton
{
    private static Singleton _instance;
    private Singleton() { }
    public static Singleton GetInstance()
    {
        if (_instance == null)
        {
            _instance = new Singleton();
        }
        return _instance;
    }
}

⚠️ Not safe for multi-threaded environments. Two threads can create two instances.

  • Pros : Lazy loaded, Simple for single-threaded use
  • Cons : Not thread-safe, Breaks in multi-threaded apps
  • Use When : In simple CLI tools, scripts or prototypes

3. Thread-Safe Singleton (With Lock)

Ensures thread safety by using lock during initialization.

public class Singleton
{
    private static Singleton _instance;
    private static readonly object _lock = new object();
    private Singleton() { }
    public static Singleton GetInstance()
    {
        lock (_lock)
        {
            if (_instance == null)
            {
                _instance = new Singleton();
            }
        }
        return _instance;
    }
}

✅ Prevents multiple threads from creating multiple instances.

  • Pros : Thread-safe, Lazy loaded
  • Cons : Slightly slower due to locking every time
  • Use When : Performance is not critical, but thread safety is required

4. Double-Checked Locking Singleton

Double-Checked Locking is a pattern used to reduce the overhead of acquiring a lock by first testing the locking criterion without actually acquiring the lock. Only if the check indicates that locking is required, the lock is acquired and then checked again. This is used to avoid the performance cost of locking every time the instance is accessed.

public sealed class Singleton
{
    private static Singleton _instance = null;
    private static readonly object _lock = new object();
    private Singleton() { }
    public static Singleton GetInstance()
    {
        if (_instance == null) // 1st check (no lock)
        {
            lock (_lock)       // Lock only if instance is null
            {
                if (_instance == null) // 2nd check (inside lock)
                {
                    _instance = new Singleton();
                }
            }
        }
        return _instance;
    }
}
  • 1st Check: Improves performance — avoids lock overhead after the instance is created
  • 2nd Check: Ensures that only one thread creates the instance, even if multiple threads pass the first check
  • Pros : Thread-safe, Locking happens only once
  • Cons : More complex and error-prone
  • Use When : High-performance multithreading and manual control needed

5. Singleton Using Lazy<T> (Best in C#)

Uses System.Lazy<T> which handles lazy loading + thread-safety internally.

public sealed class Singleton
{
    private static readonly Lazy<Singleton> _lazy =
        new Lazy<Singleton>(() => new Singleton());
    public static Singleton Instance => _lazy.Value;
    private Singleton() { }
}
  • Pros : Thread-safe, Lazy initialized, Minimal and modern
  • Cons: Slight learning curve for Lazy<T>
  • Use When: in modern .NET applications — this is the preferred pattern

6. Static Constructor Singleton

Uses a static constructor to create the instance.

public sealed class Singleton
{
    public static readonly Singleton Instance;
    static Singleton()
    {
        Instance = new Singleton();
    }
    private Singleton() { }
}
  • Pros : Thread-safe (CLR guarantees static constructor runs once), Clean, minimal
  • Cons: Not lazy (static constructor runs at first type access)
  • Use When: You want thread-safety without Lazy<T> and don’t care about laziness

7. Singleton via Dependency Injection (DI Managed)

Register the class as a Singleton in your IoC container (e.g., ASP.NET Core).

// Startup.cs or Program.cs
services.AddSingleton<ILogger, Logger>();
  • Pros : Fully testable, Easy to mock/replace in unit tests, Promotes good design (no global state)
  • Cons : Not a "true pattern" - DI container controls lifecycle, Requires IoC setup
  • Use When : In ASP.NET Core, Blazor, WPF, MAUI etc.

Comparison Table of All Singleton Variants

Variant Thread-Safe Lazy Testable Best Use Case
Eager Initialization Always-used, light objects
Lazy (Non-Thread-Safe) Prototypes, single-thread apps
Thread-Safe Lock Small to medium apps needing thread-safety
Double-Checked Locking High-perf, custom-threaded apps
Lazy Singleton 🥇 Modern C# choice (simple + safe)
Static Constructor Thread-safe without lazy init
DI-Managed Singleton Varies Enterprise apps, unit-testable, clean design

Why Use Singleton?

  • Prevent multiple instances of a class (e.g., configuration manager, database connection).
  • Save memory by reusing the same instance.
  • Provide global access to a shared resource
  • Ensures consistent state across all parts of the application.
  • Prevents resource duplication and race conditions.

When NOT to Use Singleton

Symptom Better Alternative
Need for testability & flexibility Use Dependency Injection (DI)
Multiple instances are valid Use Factory Pattern
Need for pluggable implementations Use Strategy or Interface + DI
Shared state leads to hidden bugs Refactor to explicit dependencies

When Singleton Becomes an Anti-Pattern

What is an Anti-Pattern?

An anti-pattern is a coding or design solution that:

  • Appears to be helpful or correct at first,
  • But in reality, causes more harm than good in the long run.
  • Think of it as a “bad habit in code” that looks smart early on, but leads to complexity, bugs or inflexibility later.

    Although Singleton is a well-known design pattern, overusing or misusing it leads to serious architectural problems, especially in large or testable applications. Here’s why Singleton is often considered an anti-pattern

    Symptom Why It’s Bad
    Used as a global variable Leads to tight coupling and hidden dependencies across the app.
    Difficult to test or mock Tightly binds code to an instance that’s hardcoded - breaks unit testing.
    Holds mutable shared state Can cause race conditions, side effects and debugging nightmares in concurrent scenarios.
    Used by default Chosen instead of better practices (e.g. Dependency Injection, Factory) due to convenience or misunderstanding.
    Accessed directly from multiple modules Code depends on the concrete implementation everywhere - hard to change or replace.
    Violation of SRP & SOLID principles Singleton often mixes responsibilities and breaks Open/Closed, Single Responsibility and Dependency Inversion principles.

    Singleton is fine when used deliberately, but dangerous when used by default.

    Real-World Applications

    Use Case Why Singleton?
    Logging Service Only one logger needed to write to log file/db
    App Configuration Manager Centralized control, no duplicated settings
    Cache Provider Global in-memory cache across app
    AudioManager in Game Dev One audio controller across all game scenes
    Service Locator Access services globally from different modules
    License Manager Track a single licensed instance across system

    FAQs

    What is the Singleton Design Pattern?

    The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

    Why use the Singleton Pattern?

  • To prevent multiple instances of a class (e.g. Logger, Configuration, Cache)
  • To maintain global, consistent state
  • To control resource usage (e.g. DB connection pool, file manager)
  • Is Singleton a creational or behavioral pattern?

    Creational : Because it controls how the instance is created and ensures only one object exists.

    Is Singleton a global variable?

    No, but it behaves similarly. Singleton encapsulates global access and controls instantiation, unlike uncontrolled global variables.

    Can Singleton be mocked or tested?

    Not easily, Singleton breaks testability.

    Is Singleton a good practice?

    When used intentionally and correctly, it's useful. But overuse turns it into an anti-pattern, causing tight coupling, global state and test issues.

    How is Singleton different from Static Class?

    Feature Singleton Static Class
    Instantiable ✅ Yes (1 instance) ❌ No
    Implements Interface ✅ Yes ❌ No
    Lazy Initialization ✅ Possible ❌ Always initialized early
    Testable/Mockable ✅ Yes (with effort) ❌ No
    Maintains State ✅ Yes ✅ Yes