StudyComputer ScienceDesign Patterns

The Singleton Pattern - 6 Ways to Do It in C# (And Which One Wins)

2026-03-04 16 min read Computer Science
The Singleton Pattern - 6 Ways to Do It in C# (And Which One Wins)

The Singleton Pattern: 6 Ways to Do It in C# (And Which One Wins)

Ah, the Singleton. The one pattern that every developer learns first, uses everywhere, and then spends the rest of their career arguing about whether it’s a design pattern or an anti-pattern.

Why is the Singleton pattern like the Highlander? Because there can be only one!

Here is the deal: the Singleton pattern is one of the simplest yet most debated design patterns in existence. It ensures that a class has exactly one instance throughout your application’s lifetime and provides a global access point to that instance. Sounds simple, right? Buckle up, there are six different ways to implement it in C#, and most of them have a fatal flaw.

The Singleton belongs to the Creational category of the Gang of Four design patterns. At its core, it solves two problems:

  1. Controlled access to a single instance, useful for resources that should not be duplicated (database connection pools, configuration managers, logging services).
  2. Global access point, any part of the application can reach the instance without passing it through constructors.

When Would You Actually Use This?

  • Configuration management: Application settings loaded once and shared everywhere
  • Logging: A single logging pipeline that serializes log output
  • Connection pools: Reusing expensive database connections
  • Caches: A shared in-memory cache
  • Hardware interface access: Printer spooler, device driver wrapper

The Three Traits Every Singleton Shares

No matter which implementation you choose, every Singleton has three things in common:

  1. A private constructor to prevent external instantiation
  2. A private static field holding the single instance
  3. A public static property or method to access the instance

That’s the recipe. Now let’s see six different ways to cook it.


Implementation 1: Naive Singleton (Not Thread-Safe)

This is the “Hello World” of Singletons. It is the simplest possible implementation. It works perfectly in a single-threaded application. In a multi-threaded one? It explodes.

public sealed class NaiveSingleton
{
    private static NaiveSingleton _instance;

    // Private constructor prevents external instantiation
    private NaiveSingleton()
    {
        Console.WriteLine("NaiveSingleton instance created.");
    }

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

    public void DoWork() => Console.WriteLine("Working...");
}

Why This Breaks: The Race Condition

Picture two threads running simultaneously, like two people reaching for the last donut at the same time:

Thread A: checks _instance == null  --> true
Thread B: checks _instance == null  --> true  (A hasn't created it yet!)
Thread A: creates new NaiveSingleton()
Thread B: creates new NaiveSingleton()  // SECOND instance created!

Both threads see _instance as null and both create an instance. Congratulations, you now have a “Doubleton.” The whole point of the pattern is ruined.

// Demonstrating the race condition
var tasks = Enumerable.Range(0, 10).Select(_ =>
    Task.Run(() => NaiveSingleton.Instance));
var instances = await Task.WhenAll(tasks);

// May print "NaiveSingleton instance created." more than once!
var uniqueInstances = instances.Distinct().Count();
Console.WriteLine($"Unique instances: {uniqueInstances}"); // Could be > 1

Pros: Simple and easy to understand.
Cons: Not thread-safe. Completely unsuitable for any multi-threaded application. Which is, you know, basically every modern application.

Verdict: Toy code only. Never use this in production.

Implementation 2: Simple Lock (Thread-Safe but Slow)

The most obvious fix: slap a lock on it. Problem solved, right? Well… sort of.

public sealed class LockedSingleton
{
    private static LockedSingleton _instance;
    private static readonly object _lock = new object();

    private LockedSingleton()
    {
        Console.WriteLine("LockedSingleton instance created.");
    }

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

Why It Is Slow

The lock is acquired on every single access to the Instance property, even after the instance has been created and the null check is completely pointless. In a high-throughput application where hundreds of threads access the Singleton frequently, this becomes a bottleneck. Threads queue up waiting for the lock like it is Black Friday and there is only one checkout lane.

Pros: Thread-safe, easy to understand.
Cons: Performance penalty on every access due to locking. Lock contention under high concurrency.

Verdict: Safe but sluggish. We can do better.


Implementation 3: Double-Check Locking

Double-check locking is the clever fix for the performance problem. The idea: check the instance twice, once without the lock (fast path) and once with it (safe path).

public sealed class DoubleCheckSingleton
{
    private static volatile DoubleCheckSingleton _instance;
    private static readonly object _lock = new object();

    private DoubleCheckSingleton()
    {
        Console.WriteLine("DoubleCheckSingleton instance created.");
    }

    public static DoubleCheckSingleton Instance
    {
        get
        {
            // First check (no lock) -- fast path for subsequent accesses
            if (_instance == null)
            {
                lock (_lock)
                {
                    // Second check (with lock) -- ensures only one creation
                    if (_instance == null)
                    {
                        _instance = new DoubleCheckSingleton();
                    }
                }
            }
            return _instance;
        }
    }
}

How It Actually Works

  1. First check (without lock): After the instance is created, this check returns false immediately, and the lock is never acquired. This is the “fast path” that makes subsequent accesses as fast as the naive version.

  2. Lock acquisition: If the first check passes (instance is null), only then do we pay the cost of acquiring the lock.

  3. Second check (with lock): Between the first check and acquiring the lock, another thread may have already created the instance. The second check prevents duplicates.

  4. volatile keyword: This is the sneaky critical detail. Without volatile, the compiler or CPU might reorder instructions, potentially exposing a partially constructed object to another thread. The volatile keyword ensures that reads and writes to _instance are not reordered. Forget it, and you have a bug that appears once every 10,000 runs on a specific CPU architecture. Fun times.

Pros: Thread-safe with excellent performance after initialization.
Cons: More complex to understand and implement correctly. The volatile keyword is easy to forget, and forgetting it creates the worst kind of bug, the one that almost never reproduces.

Verdict: Solid but fiddly. Good if you enjoy impressing people at code reviews.


Implementation 4: Static Initialization (Eager)

What if I told you the CLR already handles thread-safe initialization for you? Because it does.

public sealed class EagerSingleton
{
    // The CLR guarantees this is thread-safe and runs once
    private static readonly EagerSingleton _instance = new EagerSingleton();

    private EagerSingleton()
    {
        Console.WriteLine("EagerSingleton instance created.");
    }

    public static EagerSingleton Instance => _instance;
}

How It Works

The .NET runtime initializes static fields before they are first accessed. The CLR internally uses a lock to ensure the static constructor (or field initializer) runs exactly once, even if multiple threads try to access the class simultaneously. You get thread safety for free. No locks, no volatile, no double-checking.

The Catch

The instance is created as soon as the class is loaded, which might be earlier than you need it. If the Singleton is expensive to create and might never be used in certain code paths, you are paying the cost up front. This is why it is called “eager” initialization, it is like that friend who shows up to the party two hours early.

However, in practice, this is rarely a problem. The class is only loaded when first referenced, so the instance is created the first time any code touches EagerSingleton. For most applications, this is good enough.

Pros: Very simple. Thread-safe with zero effort. No locking overhead.
Cons: Eager initialization, slightly less control over when the instance is created.

Verdict: Simple and reliable. A great choice for most scenarios.

Implementation 5: Lazy\<T> (The Modern C# Approach)

The Lazy<T> class, introduced in .NET 4, provides built-in support for lazy initialization with configurable thread safety. This is the one you should probably be using.

public sealed class LazySingleton
{
    private static readonly Lazy<LazySingleton> _lazy =
        new Lazy<LazySingleton>(() =>
        {
            Console.WriteLine("LazySingleton instance created.");
            return new LazySingleton();
        });

    private LazySingleton() { }

    public static LazySingleton Instance => _lazy.Value;

    public void DoWork() => Console.WriteLine("Working...");
}

Why Lazy\<T> Is the Sweet Spot

Lazy<T> is a wrapper class provided by the .NET framework that delays the creation of an object until it is first accessed. Here is what it gives you out of the box:

  • Lazy initialization: The constructor delegate only runs when .Value is first accessed. Not a millisecond before.
  • Thread safety: By default, Lazy<T> uses LazyThreadSafetyMode.ExecutionAndPublication, which means the factory delegate is executed exactly once, even if multiple threads access .Value simultaneously.
  • Exception caching: If the factory throws an exception, Lazy<T> caches it and re-throws on subsequent access (configurable).

You can also tweak the thread safety mode depending on your needs:

// No thread safety (like the naive version, but lazy)
new Lazy<LazySingleton>(() => new LazySingleton(),
    LazyThreadSafetyMode.None);

// Thread-safe, but only one thread executes the factory
new Lazy<LazySingleton>(() => new LazySingleton(),
    LazyThreadSafetyMode.ExecutionAndPublication); // default

// Thread-safe, multiple threads may execute the factory but only one result is used
new Lazy<LazySingleton>(() => new LazySingleton(),
    LazyThreadSafetyMode.PublicationOnly);

Pros: Clean, concise, thread-safe, lazy. The framework handles all the complexity. Widely understood by C# developers.
Cons: Slight overhead from the Lazy<T> wrapper (negligible in practice, we are talking nanoseconds).

This is the recommended Singleton implementation for modern C# applications. It combines laziness, thread safety, and simplicity. If you are writing a Singleton today and not using a DI container, use this one.


Implementation 6: Static Nested Class (Bill Pugh Pattern Adapted)

This pattern is famous in the Java world (known as the Bill Pugh Singleton) and can be adapted to C#. It is a clever trick that exploits how the CLR loads nested classes.

public sealed class NestedSingleton
{
    private NestedSingleton()
    {
        Console.WriteLine("NestedSingleton instance created.");
    }

    public static NestedSingleton Instance => Nested.Instance;

    // The nested class is not loaded until Instance is accessed
    private static class Nested
    {
        // The CLR guarantees thread-safe initialization of static fields
        internal static readonly NestedSingleton Instance = new NestedSingleton();

        // Explicit static constructor tells the CLR not to mark this type
        // with the beforefieldinit flag, ensuring lazy initialization
        static Nested() { }
    }

    public void DoWork() => Console.WriteLine("Working...");
}

The Clever Part

  1. The outer class NestedSingleton does not have any static fields that would trigger early initialization.
  2. The inner class Nested contains the actual instance.
  3. Nested is only loaded by the CLR when NestedSingleton.Instance is first accessed.
  4. The explicit empty static constructor static Nested() { } prevents the CLR from marking the type with beforefieldinit, ensuring strict laziness.

It is thread-safe, lazy, and has no locking overhead. Pretty elegant, right?

Pros: Thread-safe, lazy, no locking overhead.
Cons: More complex to understand. The beforefieldinit subtlety is an obscure piece of CLR trivia that most developers have never heard of. In modern C#, Lazy<T> communicates the same intent much more clearly.

Verdict: Technically excellent but unnecessarily clever. Use Lazy<T> instead unless you enjoy explaining beforefieldinit to confused juniors during code review.


The Showdown: Thread Safety Comparison

ImplementationThread-SafeLazyComplexityPerformance
NaiveNoYesVery LowFast (unsafe)
Simple LockYesYesLowSlow (lock on every access)
Double-Check LockingYesYesMediumFast after init
Static InitializationYesNo*Very LowFast
Lazy\<T>YesYesLowFast
Nested ClassYesYesMediumFast

Static initialization is effectively lazy in most scenarios since the class is loaded on first reference, but technically the CLR controls the timing.

The Winner: Lazy<T>. It is lazy, thread-safe, simple, and the framework handles all the complexity for you. Unless you are using a DI container (see below), this is your go-to.


The Elephant in the Room: “Singleton Is an Anti-Pattern!”

Alright, let’s address this. If you have spent any time on Stack Overflow, Reddit, or Twitter (sorry, “X”), you have seen the heated debates. Some developers will tell you the Singleton pattern is the worst thing since goto. Others use it everywhere. Who is right?

The truth, as usual, is somewhere in the middle. Singleton is not inherently evil, but it IS the most misused design pattern. Here is why it can cause real pain:

1. Hidden Dependencies

Singleton creates invisible dependencies. When a method calls Logger.Instance.Log(...) internally, nothing in its signature reveals this dependency. It is like a function that secretly calls the database, you won’t know until it blows up in testing.

// Hidden dependency -- not visible from the method signature
public class OrderService
{
    public void PlaceOrder(Order order)
    {
        // Where does Logger come from? What does this method actually depend on?
        Logger.Instance.Log("Order placed");
        DatabaseSingleton.Instance.Save(order);
        EmailSingleton.Instance.Send(order.CustomerEmail, "Order confirmed");
    }
}

2. Testing Becomes Painful

Singletons make unit testing a nightmare because you cannot substitute a mock implementation. Tests become coupled to the real Singleton, and because the instance persists across tests, one test can pollute another.

// This is hard to test -- you can't mock Logger.Instance
[Test]
public void PlaceOrder_ShouldSaveToDatabase()
{
    var service = new OrderService();
    service.PlaceOrder(new Order()); // Calls real Logger, real Database, real Email!
}

3. Global Mutable State

A Singleton is essentially global state wearing a fancy hat. Any part of the application can access and modify it, making it difficult to reason about the system’s state at any point in time. In concurrent applications, this is a recipe for race conditions.

4. Responsibility Magnet

Because Singletons are accessible from everywhere, they tend to accumulate responsibilities over time. Your innocent little Logger gradually becomes a 2,000-line god object that also handles configuration, metrics, and for some reason, date formatting.


Singleton vs. Static Class

A question that comes up constantly: “Why not just use a static class instead?”

FeatureSingletonStatic Class
Implements interfacesYesNo
Can be passed as parameterYesNo
Supports polymorphismYesNo
Lazy initializationYes (configurable)No (loaded when first accessed)
Can be mocked in testsYes (via interface)No
InheritanceYes (sealed by convention)No
Instance stateYesStatic fields only
Dependency injectionYesNo
Lifetime controlPossible (with DI container)Application lifetime only

Use a Static Class When:
- You have a collection of pure utility methods with no state (like Math.Max())
- You do not need polymorphism, testing with mocks, or dependency injection
- The functionality is truly stateless

Use a Singleton When:
- You need a single instance with state that implements an interface
- You need to inject the instance for testability
- You might later need to swap implementations (e.g., mock vs. real)


Making Singleton Testable: The Interface Approach

The key to keeping your Singleton from becoming a testing nightmare is to program against an interface, not the concrete Singleton class.

// Step 1: Define the interface
public interface ILogger
{
    void Log(string message);
    IReadOnlyList<string> GetLogs();
}

// Step 2: Implement the Singleton behind the interface
public sealed class Logger : ILogger
{
    private static readonly Lazy<Logger> _lazy =
        new Lazy<Logger>(() => new Logger());

    private readonly List<string> _logs = new();

    private Logger() { }

    public static Logger Instance => _lazy.Value;

    public void Log(string message)
    {
        _logs.Add($"[{DateTime.UtcNow:HH:mm:ss}] {message}");
        Console.WriteLine(_logs[^1]);
    }

    public IReadOnlyList<string> GetLogs() => _logs.AsReadOnly();
}

// Step 3: Depend on the interface, not the Singleton
public class OrderService
{
    private readonly ILogger _logger;

    // Constructor accepts the interface -- Singleton injected from outside
    public OrderService(ILogger logger)
    {
        _logger = logger;
    }

    public void PlaceOrder(string item)
    {
        _logger.Log($"Order placed for {item}");
    }
}

// Step 4: In production, inject the Singleton
var productionService = new OrderService(Logger.Instance);
productionService.PlaceOrder("Laptop");

// Step 5: In tests, inject a mock
public class MockLogger : ILogger
{
    public List<string> Messages { get; } = new();

    public void Log(string message) => Messages.Add(message);
    public IReadOnlyList<string> GetLogs() => Messages.AsReadOnly();
}

[Test]
public void PlaceOrder_ShouldLogOrderPlacement()
{
    // Arrange
    var mockLogger = new MockLogger();
    var service = new OrderService(mockLogger);

    // Act
    service.PlaceOrder("Keyboard");

    // Assert
    Assert.That(mockLogger.Messages.Count, Is.EqualTo(1));
    Assert.That(mockLogger.Messages[0], Does.Contain("Keyboard"));
}

Clean, testable, and the Singleton is still there behind the scenes when you need it.

The REAL Modern Approach: Let the DI Container Handle It

In modern .NET applications using dependency injection, you rarely need to implement Singleton manually. The DI container manages the lifetime for you, and it does a much better job:

// In Startup.cs or Program.cs
builder.Services.AddSingleton<ILogger, Logger>();
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
builder.Services.AddSingleton<IConfiguration>(Configuration);

// The DI container ensures only one instance exists
// and injects it wherever ILogger is requested
public class OrderController : ControllerBase
{
    private readonly ILogger _logger;

    public OrderController(ILogger logger)
    {
        _logger = logger; // Same instance every time
    }
}

Best practice in modern C#: Let your DI container manage Singleton lifetime. Register your service as a Singleton in the container, depend on interfaces, and let the framework handle the rest. The manual Singleton pattern is still useful for understanding the concept and for scenarios outside of DI (libraries, static utilities, game dev).

The Decision Framework

Before you reach for the Singleton pattern, run through this checklist:

  1. Do I really need a single instance? If not, do not use Singleton. Seriously, ask this question twice.
  2. Am I using a DI container? If yes, register as Singleton in the container, do not implement the pattern manually.
  3. Am I implementing it manually? Use Lazy<T> for the cleanest, thread-safe, lazy implementation.
  4. Am I exposing the Singleton directly? Put an interface in front of it for testability.

The Singleton pattern is a tool. Like any tool, its value depends on using it appropriately. A hammer is great for nails but terrible for screws. Understand it deeply, apply it judiciously, and always keep testability in mind.

And for the love of clean code, stop putting business logic in your Singletons.

PreviousProbability Theory - From Coin Flips to Bayes' Theorem