Singleton is the most famous pattern in the book, and probably the most argued about. It makes sure a class has exactly one instance and gives everyone one way to reach it. In CornerStore it cleans up some genuinely risky global state. But it also teaches a lesson you can see in the size of its diff.

   
Category Creational
Smell fixed S05: Configuration and logging are mutable static state
Code patterns/creational/singleton

The Scene

Halfway through the trading day, someone changes a setting:

[055] config.set key=fraudThreshold value=-1
  fraudThreshold set to -1
[056] checkout customer=cu-1 payment=card
  checkout rejected: fraud score 88 exceeds threshold

One line, and suddenly every checkout fails the fraud check. The next step sets the threshold back to 100 and business carries on.

In the scenario that’s deliberate. In a real system, the question is: who’s allowed to change a setting that affects everyone, and how would you even know they did?


The Smell

The baseline keeps configuration in a static class with public fields:

// SMELL S05: global settings in a static class with public mutable fields, set from anywhere
internal static class Config
{
    public static int Seed = 0;
    public static int FraudThreshold = 80;
    public static int LowStockLevel = 3;

And the transcript logger is the same story:

// SMELL S05: global transcript writer in a static class with a public mutable field
internal static class Log
{
    public static TextWriter Out = TextWriter.Null;

Anyone, anywhere, can write Config.FraudThreshold = -1 or swap Log.Out for a different writer. There’s no single place that controls changes, nothing to stop two threads writing at once, and no way to add a check without finding every caller.


The Idea

Singleton turns “a bag of global fields” into one real object with a private constructor, a single shared instance and methods that control how its state changes.

Think of a building’s main reception desk. There’s exactly one, everyone knows where it is, and if you want something changed you ask the desk. You don’t walk behind it and edit the visitor log yourself.


The Refactor

Config becomes a sealed class that nobody else can construct. Its one instance is created lazily and safely, even if several threads ask at the same moment:

private static readonly Lazy<Config> singleton =
    new(static () => new Config(), LazyThreadSafetyMode.ExecutionAndPublication);
public static Config Instance => singleton.Value;

The fields are now private. Changes go through methods that take a lock:

public void SetSeed(int value)
{
    lock (sync)
    {
        seed = value;
    }
}

The logger gets the same treatment, and it now refuses a null writer:

public static Log Instance => singleton.Value;

public void SetOutput(TextWriter writer)
{
    ArgumentNullException.ThrowIfNull(writer);
    lock (sync)
    {
        output = writer;
    }
}
classDiagram
    class Config {
        -Lazy~Config~ singleton
        -object sync
        +Instance
        +Seed
        +FraudThreshold
        +LowStockLevel
        +SetSeed(value)
        +SetFraudThreshold(value)
        +SetLowStockLevel(value)
        +Reset()
    }
    class Log {
        -Lazy~Log~ singleton
        -object sync
        -TextWriter output
        +Instance
        +SetOutput(writer)
        +ResetOutput()
        +Line(text)
        +Info(text)
    }
    class ScenarioRunner
    class StoreConsole
    class OrderValidator
    class FraudScorer

    ScenarioRunner --> Config : resets and seeds
    ScenarioRunner --> Log : sets and resets output
    StoreConsole --> Config : changes settings
    OrderValidator --> Config : reads fraud threshold
    FraudScorer --> Config : reads seed
    StoreConsole ..> Log : writes transcript

There’s now one door for every change. If you ever need to validate a threshold, audit who changed it, or raise an event, there’s exactly one method to put it in.


What It Cost

Here’s the lesson I promised. Singleton touched 22 files, the most in the whole series: about 277 lines added and 101 removed.

Why so many? Because every single caller had to change from Log.Info(...) to Log.Instance.Info(...), and from Config.FraudThreshold to Config.Instance.FraudThreshold. Payment processors, notification senders, widgets, the loyalty service, the analytics service. Sixteen source files reference Log.Instance alone.

The diff is a map of the coupling. Global state doesn’t stay in one place. It reaches into everything, quietly, and you only see how far when you try to change it. That’s not a flaw in the refactor. It’s the most honest picture of the original smell you’ll get.


When Not to Use It

Singleton gets a hard time in modern development, and mostly for good reasons:

  • It’s still global state. A better-behaved global, but global. Any class can reach it without declaring the dependency, so you can’t tell what a class needs by looking at its constructor.
  • It makes tests share state. Two tests that both change Config.Instance can affect each other. CornerStore gets away with it because the scenario runner resets everything before each run.
  • The state belongs to something smaller. Settings for one request, one tenant or one test shouldn’t live in a process-wide instance.

The modern .NET way

In modern .NET, the usual answer is a dependency injection singleton lifetime:

services.AddSingleton(Config.Instance);

You still get one shared instance, but classes receive it through their constructor instead of reaching for a global property. That makes dependencies visible and lets tests swap in their own instance. The README for this solution covers this, and why you shouldn’t keep both access paths side by side.

My rule of thumb: understand Singleton, because you’ll meet it in every legacy codebase. Then prefer letting your container own the “only one” decision.


Easily Confused With

A static class. It’s tempting to think a static class is a singleton. The baseline proves it isn’t. A static class has no instance, so it can’t implement an interface, can’t be passed as a parameter, can’t be swapped in a test and can’t control how its fields change. Singleton gives you a real object, which is also what makes the step to dependency injection possible.


Try It Yourself

dotnet build patterns/creational/singleton/Singleton.slnx
dotnet test patterns/creational/singleton/Singleton.slnx
dotnet run --project patterns/creational/singleton/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json

Open DIFF.md and scroll. Every one-line change is a place the global state had reached. It’s a surprisingly useful exercise to run on your own codebase.

The Singleton README has the participant table and a sequence diagram of how the scenario runner sets things up.

That wraps up the Creational patterns. Next, we move on to how objects fit together.


Previous: Prototype · Next: Adapter