Singleton Pattern
Jon Skeet
Programming
Software Development
Design Patterns

Singleton by Jon Skeet clarification

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Jon Skeet’s singleton guidance is popular because it favors simple, correct patterns over lock-heavy boilerplate. The core idea is that CLR type initialization already provides thread-safe guarantees for static initialization. In most modern C# code, straightforward static initialization or Lazy<T> is the cleanest approach.

What a Singleton Should Guarantee

A singleton typically means:

  • One instance per process.
  • Global access point.
  • Controlled construction path.

It does not automatically make all mutable operations thread-safe. Construction safety and runtime state safety are separate concerns.

Use singleton only when one logical shared instance truly represents domain design. If state should vary by request or user, singleton is usually wrong.

Simple Static Initialization Pattern

The simplest Skeet-aligned pattern is static readonly initialization.

csharp
1public sealed class AppConfig
2{
3    private static readonly AppConfig _instance = new AppConfig();
4
5    public static AppConfig Instance => _instance;
6
7    private AppConfig()
8    {
9        Region = "us-east";
10    }
11
12    public string Region { get; }
13}

Why this works:

  • Type initialization runs once.
  • CLR guarantees thread-safe static initialization.
  • No manual lock complexity.

For most applications, this is enough.

Lazy<T> for Deferred Initialization

If instance creation is expensive and should be delayed until first use, Lazy<T> is a strong option.

csharp
1using System;
2
3public sealed class MetricsRegistry
4{
5    private static readonly Lazy<MetricsRegistry> _instance =
6        new Lazy<MetricsRegistry>(() => new MetricsRegistry());
7
8    public static MetricsRegistry Instance => _instance.Value;
9
10    private MetricsRegistry()
11    {
12        // expensive initialization
13    }
14}

This pattern preserves thread safety while avoiding startup cost until needed.

Nested Holder Pattern

Another classic approach uses a nested static type.

csharp
1public sealed class JobScheduler
2{
3    public static JobScheduler Instance => Holder.Instance;
4
5    private JobScheduler() { }
6
7    private static class Holder
8    {
9        internal static readonly JobScheduler Instance = new JobScheduler();
10    }
11}

This is valid and lazy, though many teams now prefer Lazy<T> for readability.

Why Double-Checked Locking Is Usually Unnecessary

Legacy tutorials often show double-checked locking. It can be correct if done carefully, but it is harder to read and easier to misuse.

In most cases, replace it with static initialization or Lazy<T> and remove manual synchronization noise.

Simpler code with same guarantees is usually the best engineering tradeoff.

Singleton Versus Dependency Injection Lifetime

In DI-driven applications, you can often get singleton lifetime through container configuration instead of static access.

csharp
services.AddSingleton<IClock, UtcClock>();

Benefits:

  • Easier mocking in tests.
  • Better control over composition root.
  • Fewer hidden global dependencies.

If your project already uses DI extensively, container-managed singleton is often preferable.

Mutable State and Concurrency

Even with thread-safe construction, mutable singleton fields can still race.

csharp
1using System.Collections.Concurrent;
2
3public sealed class Cache
4{
5    private static readonly Cache _instance = new Cache();
6    public static Cache Instance => _instance;
7
8    private readonly ConcurrentDictionary<string, int> _map = new();
9
10    private Cache() { }
11
12    public void Set(string key, int value) => _map[key] = value;
13    public bool TryGet(string key, out int value) => _map.TryGetValue(key, out value);
14}

Design mutable singleton state with explicit thread-safe structures and invariants.

Testing Considerations

Static singletons can leak state across tests. Mitigation options:

  • Keep singleton immutable.
  • Add controlled reset mechanism for test environment only.
  • Prefer DI lifetimes in test-heavy codebases.

Testing pain is often a signal that static global access is overused.

Common Pitfalls

  • Using singleton as default dependency pattern everywhere.
  • Assuming singleton creation safety implies mutable-state safety.
  • Adding heavy I/O work in singleton constructor and causing startup latency surprises.
  • Reintroducing manual lock patterns without need.
  • Choosing singleton for components that should be scoped or transient.

Summary

  • Prefer simple CLR-backed static initialization for most singleton cases.
  • Use Lazy<T> when deferred creation is required.
  • Avoid manual double-checked locking unless constraints demand it.
  • Treat mutable singleton state as a separate concurrency design problem.
  • In DI-based systems, consider container singletons for better testability and explicit dependencies.

Course illustration
Course illustration

All Rights Reserved.