C#
singleton pattern
design patterns
software engineering
object-oriented programming

What is a singleton in C?

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

Introduction

A singleton is a design pattern that ensures a class has exactly one shared instance and exposes a global way to reach it. In C#, the pattern is usually implemented with a private constructor and a static property or field. The mechanics are simple, but the design tradeoff matters more than the syntax.

The Core Shape of a Singleton in C#

The basic idea is to block direct construction from outside the class and keep one shared instance inside the type itself.

csharp
1public sealed class AppSettings
2{
3    private static readonly AppSettings _instance = new AppSettings();
4
5    public static AppSettings Instance => _instance;
6
7    public string Theme { get; set; } = "Light";
8
9    private AppSettings()
10    {
11    }
12}

Usage:

csharp
AppSettings.Instance.Theme = "Dark";
Console.WriteLine(AppSettings.Instance.Theme);

This works because the CLR initializes static fields only once per application domain. The sealed keyword is also helpful because it prevents subclassing, which could otherwise complicate the "exactly one instance" rule.

Lazy Initialization and Thread Safety

Sometimes you do not want to create the singleton until it is first needed. In modern C#, Lazy<T> is the cleanest way to do that safely.

csharp
1using System;
2
3public sealed class Logger
4{
5    private static readonly Lazy<Logger> _instance =
6        new Lazy<Logger>(() => new Logger());
7
8    public static Logger Instance => _instance.Value;
9
10    private Logger()
11    {
12    }
13
14    public void Write(string message)
15    {
16        Console.WriteLine($"LOG: {message}");
17    }
18}

This version is both lazy and thread-safe without custom locking code. That is usually better than hand-written double-checked locking unless you have a very specific reason to manage the synchronization yourself.

When a Singleton Can Make Sense

Singletons are sometimes reasonable when the application truly has one shared coordinator, such as:

  • a configuration cache
  • a process-wide metrics collector
  • a lightweight logger wrapper
  • a shared registry with one global view

The pattern is most defensible when the object represents a real single concept in the process and when global access is actually helpful rather than just convenient.

Why Singletons Are Often Overused

The pattern is popular because it is easy to write, but that does not mean it is always a good design. A singleton is still global mutable state if the instance can change internally. That makes code harder to test, harder to reason about, and more tightly coupled.

For example, this class compiles fine but is painful in tests because every test shares the same underlying state:

csharp
1public sealed class Counter
2{
3    private static readonly Counter _instance = new Counter();
4    public static Counter Instance => _instance;
5
6    public int Value { get; private set; }
7
8    private Counter() { }
9
10    public void Increment() => Value++;
11    public void Reset() => Value = 0;
12}

If one test increments the counter and another expects zero, they now interfere unless the shared state is carefully reset.

Prefer Dependency Injection When You Can

In modern C# applications, especially ASP.NET Core, dependency injection often gives the same "one shared service" behavior with better testability. A service can be registered as a singleton in the container without hard-coding global access into the type itself.

csharp
builder.Services.AddSingleton<ClockService>();

That still creates one shared instance for the application, but consumers receive it through constructor injection. This keeps dependencies explicit and makes mocking or replacement easier in tests.

Common Pitfalls

One common mistake is using a singleton just to avoid passing dependencies around. That usually hides coupling instead of reducing it.

Another mistake is forgetting that shared mutable state is a concurrency concern. A singleton does not become thread-safe just because there is only one instance.

Developers also sometimes write manual locking code when static readonly initialization or Lazy<T> would be simpler and safer.

Finally, a singleton can become a dumping ground for unrelated behavior. Once a type is globally accessible, teams often keep adding responsibilities until it turns into an unmaintainable god object.

Summary

  • A singleton ensures one shared instance of a class and exposes a global access point.
  • In C#, private constructors plus static members are the standard implementation pattern.
  • 'Lazy<T> is a clean way to get lazy, thread-safe initialization.'
  • Singletons are easy to write but can introduce hidden coupling and shared-state problems.
  • Prefer dependency injection when you want one shared service without hard-coded global access.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

All Rights Reserved.