Skip to content

// Design Patterns · 05

Singleton

Needs exactly one instanceleads toOne shared, container-owned

Exactly one instance; let the container own it, not a static.

How it fits together

Singleton structureQuoteService«external»IoC containerregistered as singletonCurrencyRateCachethread-safe map of ratesInvoiceServicePricingJoboncesame instance
createsuses / calls
  • IoC container creates CurrencyRateCache (once)
  • QuoteService uses / calls CurrencyRateCache
  • InvoiceService uses / calls CurrencyRateCache (same instance)
  • PricingJob uses / calls CurrencyRateCache

Spot it

  • An expensive client created on every call
  • Sockets exhausted by a new HTTP client per call
  • Several in-memory caches of the same data

It costs

Global shared state: hidden coupling, harder tests, and thread safety is on you.

In code

public sealed class CurrencyRateCache{    private readonly ConcurrentDictionary<string, decimal> _rates =        new(StringComparer.OrdinalIgnoreCase);​    public decimal? Get(string currency) =>        _rates.TryGetValue(currency, out var rate) ? rate : null;​    public void Set(string currency, decimal rate) => _rates[currency] = rate;}​// Composition root: services.AddSingleton<CurrencyRateCache>();
Go deeperThe full explanation and what to ask for · 1 min readExpand

The classic version is a private constructor and a static Instance. Static or lazy initialisation gives thread-safe creation, but the result is still a global variable that callers reach for instead of declaring.

A singleton that holds a scoped service, such as a database session, is a captive dependency: the scoped object lives forever and is shared across requests. Many DI containers can validate scopes at startup and catch this.

Prefer a container-managed singleton: one instance per container, injected like anything else and replaceable in tests. It is one per process, though; with several instances behind a load balancer you have several singletons, and cluster-wide uniqueness needs a lock or leader election.

Ask for it

  • singleton lifetime
  • shared instance
  • thread safety
  • captive dependency
  • lazy initialisation

Register CurrencyRateCache as a singleton instead of its static Instance property, make it thread-safe with a concurrent map, and check it holds no scoped dependencies such as a database session (resolve a scope factory if it needs one).