30% OFF - Use code HANDSON30 for 30% off any course or Dometrain Pro! Browse courses →
  • Courses
  • Learning Paths
  • Blogs
  • Authors
  • Leaderboard
  • Dometrain Pro
  • Shopping Basket

    Your basket is empty

  • Business Portal
  • Design Patterns in C#: Singleton with examples

    August 14, 2025
    Listen to this blog 4m 6s
    Sign in to unlock the audio version of this article
    Sign in to listen

    The Singleton pattern means one instance for the whole app and a global access point to it. Sounds good? It does, though it can also cause hidden coupling and testing pain if used carelessly. This post shows practical C# implementations, the thread‑safety details that matter, and safer alternatives that play nicely with DI and tests.


    What “one instance” actually implies

    • Creation: exactly once per process (or once per “container” scope if you design it that way).
    • Access: everyone reaches the same object.
    • State: if you keep mutable state, every caller shares it.

    Good fits: pure services with no per‑request state (formatters, ID generators, config readers, in‑memory lookup tables).
    Bad fits: request‑bound work, multi‑tenant data, anything that holds connections per request (e.g., DbContext).


    Four ways to do it in C#

    1) Static holder (eager)

    public sealed class AppSettings
    {
        private AppSettings() { /* read config files, env vars, etc. */ }
    
        public static readonly AppSettings Instance = new AppSettings();
    
        public string Name { get; init; } = "MyApp";
    }
    

    Pros: tiny and thread‑safe (CLR initializes type statics once).
    Cons: eager creation; hard to override in tests; no DI.


    2) Lazy (deferred, thread‑safe)

    public sealed class TokenCache
    {
        private TokenCache() { }
    
        private static readonly Lazy<TokenCache> _lazy =
            new(() => new TokenCache(), isThreadSafe: true);
    
        public static TokenCache Instance => _lazy.Value;
    
        private readonly ConcurrentDictionary<string, string> _tokens = new();
    
        public void Set(string key, string token) => _tokens[key] = token;
        public bool TryGet(string key, out string token) => _tokens.TryGetValue(key, out token);
    }
    

    Pros: creates on first use; handles races for you.
    Cons: still global; hard to substitute in tests.


    3) Double‑checked locking (for learning, not needed with Lazy<T>)

    public sealed class Metrics
    {
        private static Metrics? _instance;
        private static readonly object _lock = new();
    
        private Metrics() { }
    
        public static Metrics Instance
        {
            get
            {
                if (_instance is null)                 // first check
                {
                    lock (_lock)
                    {
                        _instance ??= new Metrics();   // second check
                    }
                }
                return _instance;
            }
        }
    }
    

    Note: use Lazy<T> instead. The CLR got the hard parts right already.


    4) Preferred in apps: DI container singleton

    Let the built‑in container own the lifetime. You keep testability and can swap implementations.

    Service and registration

    public interface ISystemClock { DateTime UtcNow { get; } }
    public sealed class SystemClock : ISystemClock { public DateTime UtcNow => DateTime.UtcNow; }
    
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddSingleton<ISystemClock, SystemClock>();
    
    var app = builder.Build();
    

    Minimal API endpoint

    app.MapGet("/now", (ISystemClock clock) => new { utc = clock.UtcNow });
    app.Run();
    

    Benefits: one instance per host, easy to mock in tests, no global statics leaked into business code.


    Thread‑safety and state

    Singleton != thread‑safe by magic. If the object mutates shared state, guard it:

    public sealed class MonotonicId
    {
        private long _value;
    
        public long Next() => Interlocked.Increment(ref _value);
    }
    

    Collections: prefer ConcurrentDictionary/ConcurrentQueue or protect standard collections with a lock.

    Avoid doing I/O or async work in constructors. If you must initialize asynchronously, use an explicit InitAsync() that runs during startup.

    public interface IWarmup { Task InitAsync(CancellationToken ct = default); }
    
    public sealed class HeavyLookup : IWarmup, IAsyncDisposable
    {
        public async Task InitAsync(CancellationToken ct = default)
        {
            // load data here
            await Task.CompletedTask;
        }
    
        public ValueTask DisposeAsync() => ValueTask.CompletedTask;
    }
    

    Register and warm up on start:

    builder.Services.AddSingleton<HeavyLookup>();
    
    var app = builder.Build();
    
    using (var scope = app.Services.CreateScope())
    {
        var warm = scope.ServiceProvider.GetRequiredService<HeavyLookup>();
        await warm.InitAsync();
    }
    
    app.Run();
    

    Disposal and lifetime

    If your singleton implements IDisposable/IAsyncDisposable, and you register it with DI, ASP.NET Core will dispose it when the host shuts down. Don’t forget to release timers, file handles, and sockets there.


    Testing options

    Testing the DI singleton (easy)

    public sealed class FakeClock : ISystemClock
    {
        public DateTime UtcNow { get; set; } = new(2025, 1, 1, 12, 0, 0, DateTimeKind.Utc);
    }
    
    [Fact]
    public void Endpoint_uses_injected_clock()
    {
        var builder = WebApplication.CreateBuilder();
        builder.Services.AddSingleton<ISystemClock>(new FakeClock());
        var app = builder.Build();
    
        // call the handler directly (no server needed)
        var clock = app.Services.GetRequiredService<ISystemClock>();
        clock.UtcNow.ShouldBe(new DateTime(2025, 1, 1, 12, 0, 0, DateTimeKind.Utc));
    }
    

    Testing a static singleton (awkward)

    Statically held instances are hard to replace. If you must, add an internal setter guarded for tests:

    public sealed class Config
    {
        private Config() { }
        private static readonly Lazy<Config> _lazy = new(() => new Config());
        public static Config Instance { get; internal set; } = _lazy.Value;
    }
    

    Now your tests can do Config.Instance = new TestConfig();. Prefer DI instead.

    Use Shouldly for assertions if you want natural syntax (no extra license constraints).


    Common mistakes (and fixes)

    • Hidden global state sneaks into every module → Prefer DI + interface over Class.Instance.
    • Captive dependency: singleton depends on scoped services like DbContext → either make it scoped or inject a factory.
    • Mutable singleton with no sync → add locks or concurrent collections; better: keep singletons stateless.
    • Multiple singletons that secretly depend on each other → you just created order‑dependent init bugs. Untangle responsibilities.
    • Using singleton as a data cache without expiry → add TTLs or invalidation; consider a proper cache instead.

    When you actually want a singleton

    • System time abstraction (ISystemClock), random ID generators.
    • Configuration readers, feature flags snapshot (if you reload, expose a safe update path).
    • Pure function services with no per‑request state.
    • Small in‑memory maps that are read often and rebuilt on change.

    If you feel tempted to put a repository or an ORM context in a singleton, that’s a smell.


    A small end‑to‑end example

    Service

    public interface IFeatureFlags { bool IsEnabled(string key); }
    
    public sealed class InMemoryFlags(IDictionary<string, bool> flags) : IFeatureFlags
    {
        private readonly IReadOnlyDictionary<string, bool> _flags = new Dictionary<string, bool>(flags);
    
        public bool IsEnabled(string key) => _flags.TryGetValue(key, out var on) && on;
    }
    

    Registration

    var flags = new Dictionary<string, bool>
    {
        ["checkout:new-flow"] = true,
        ["search:beta"] = false
    };
    
    builder.Services.AddSingleton<IFeatureFlags>(_ => new InMemoryFlags(flags));
    

    Endpoint

    app.MapGet("/flags/{key}", (string key, IFeatureFlags ff) => new { key, enabled = ff.IsEnabled(key) });
    

    One instance shared, easy to replace in tests, no static globals.


    Quick checklist

    • [ ] Prefer DI‑managed singletons over static singletons.
    • [ ] Keep singletons stateless or make them thread‑safe.
    • [ ] No async work in constructors; add an InitAsync when needed.
    • [ ] Watch for captive dependencies (singleton depending on scoped).
    • [ ] If you must expose a static instance, make it swap‑able for tests.

    About the Author

    Nick Chapsas

    Nick Chapsas

    Nick Chapsas is a .NET & C# content creator, educator and a Microsoft MVP for Developer Technologies with years of experience in Software Engineering and Engineering Management.

    He has worked for some of the biggest companies in the world, building systems that served millions of users and tens of thousands of requests per second.

    Nick creates free content on YouTube and is the host of the Keep Coding Podcast.

    View all courses by Nick Chapsas

    What's New

    Getting Started: Model Context Protocol (MCP)
    course

    Getting Started: Model Context Protocol (MCP)

    Learn how to get started with the Model Context Protocol (MCP) and integrate it into your applications.

    Learn more about Getting Started: Model Context Protocol (MCP)
    Hands-On: Learn TypeScript
    course

    Hands-On: Learn TypeScript

    Learn TypeScript through hands-on coding exercises. Practice what you learn with interactive challenges designed for every level.

    Learn more about Hands-On: Learn TypeScript
    Hands-On: Learn JavaScript
    course

    Hands-On: Learn JavaScript

    Learn JavaScript through hands-on coding exercises. Practice what you learn with interactive challenges designed for every level.

    Learn more about Hands-On: Learn JavaScript
    Hands-On: Data Structures & Algorithms in C#
    course

    Hands-On: Data Structures & Algorithms in C#

    Master data structures and algorithms through hands-on coding exercises in C#. Free to enroll for 7 days!

    Learn more about Hands-On: Data Structures & Algorithms in C#
    Blogsmith.ai
    feature

    Blogsmith.ai

    Turn your videos into blogs and newsletters with AI. Check out our new product at blogsmith.ai.

    Learn more about Blogsmith.ai
    Leaderboard
    feature

    Leaderboard

    See how you stack up against other learners. Track your progress, climb the ranks, and compete with the Dometrain community.

    Learn more about Leaderboard
    Hands-On: Learn PostgreSQL
    course

    Hands-On: Learn PostgreSQL

    Learn PostgreSQL through hands-on coding exercises. Practice what you learn with interactive challenges designed for every level.

    Learn more about Hands-On: Learn PostgreSQL
    Free Hands-On: C# for Beginners
    course

    Free Hands-On: C# for Beginners

    Learn C# through hands-on coding exercises. Practice what you learn with interactive challenges designed for everyone, from beginners to experts.

    Learn more about Free Hands-On: C# for Beginners
    Getting Started: AI for .NET Developers
    course

    Getting Started: AI for .NET Developers

    Get started with integrating AI into your .NET applications effectively using the latest LLM best practices.

    Learn more about Getting Started: AI for .NET Developers
    Getting Started: Building .NET Applications on AWS
    course

    Getting Started: Building .NET Applications on AWS

    Learn how to build and deploy .NET applications on AWS using CDK, Lambda, DynamoDB, S3, and more.

    Learn more about Getting Started: Building .NET Applications on AWS
    What's new in C# 14
    blog

    What's new in C# 14

    This guide covers every new C# 14 feature, explains its benefits, and provides practical code examples to help you navigate how you can use them.

    Learn more about What's new in C# 14
    Let's Build It: AI Chatbot with RAG in .NET Using Your Data
    course

    Let's Build It: AI Chatbot with RAG in .NET Using Your Data

    Build a Retrieval-Augmented Generation (RAG) chatbot that can answer questions using your data.

    Learn more about Let's Build It: AI Chatbot with RAG in .NET Using Your Data
    From Zero to Hero: SignalR in .NET
    course

    From Zero to Hero: SignalR in .NET

    Enable enterprise-grade real-time communication for your web apps with SignalR.

    Learn more about From Zero to Hero: SignalR in .NET
    Deep Dive: Solution Architecture
    course

    Deep Dive: Solution Architecture

    Master solution architecture and turn business needs into scalable, maintainable systems.

    Learn more about Deep Dive: Solution Architecture
    Migrating: ASP.NET Web APIs to ASP.NET Core
    course

    Migrating: ASP.NET Web APIs to ASP.NET Core

    A step-by-step process to migrate ASP.NET Web APIs from .NET Framework to ASP.NET Core.

    Learn more about Migrating: ASP.NET Web APIs to ASP.NET Core
    Getting Started: Caching in .NET
    course

    Getting Started: Caching in .NET

    Let's make the hardest thing in programming easy for .NET software engineers.

    Learn more about Getting Started: Caching in .NET
    From Zero to Hero: Testing with xUnit in C#
    course

    From Zero to Hero: Testing with xUnit in C#

    Learn how to test any codebase in .NET with the latest version of xUnit, the industry-standard testing library.

    Learn more about From Zero to Hero: Testing with xUnit in C#
    Create a ChatGPT Console AI Chatbot in C#
    blog

    Create a ChatGPT Console AI Chatbot in C#

    This walkthrough is your hands-on entry point to create a basic C# console application that talks to ChatGPT using the OpenAI API.

    Learn more about Create a ChatGPT Console AI Chatbot in C#