// Design Patterns · 05
Singleton
Exactly one instance; let the container own it, not a static.
How it fits together
- 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 readExpandCollapse
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).