Dear Singleton, It’s Not Me—It’s Your Single Point of Failure

By Fuat Can Koseoglu,

Keep singletons stateless or keep them out. What went wrong • PR smell: GameManager.Instance.CurrentUser mutated by six controllers; side effects everywhere. • Drift: Week 1 stateless config → Week 2 “convenient” user store → Month 2 global mutable state + hidden coupling + races. • Result: Singleton became a global variable—testing nightmare. Correct roles • Singleton (only valid use): immutable config/provider only (no mutable fields, no event subscriptions). • Services (e.g., IUserService, IStatsService): clear contracts; no global state; dependencies injected. • Composition root: Bootstrapper/Installer/SceneLoader builds the object graph and manages lifecycles. Services never touch global state. Real production costs (the bill you’ll pay) • Races: concurrent reads/writes on globals → nondeterministic bugs. • Flaky tests: shared state → order dependence and brittle suites. • Hidden deps: Singleton.Instance inside logic → untestable edges. • Locks in hot paths: _staticLock throttles frame time; deadlock risk. • Leaks: long-lived singletons pin references → memory growth. PR litmus (merge blockers) • No globals: Fail if business logic reads/writes static mutable state or uses AsyncLocal/[ThreadStatic]. (Immutable config OK.) • Constructor injection: Fail if Singleton.Instance appears outside composition root. • Thread safety: Fail if correctness relies on global locks; prefer immutable data or per-request state. • Stateless services: Fail if services keep mutable fields beyond injected deps; hot-path per-call allocations = 0 (CI) • Service isolation: Fail if services coordinate via singletons; orchestrate via composition root/mediator. • Time/Random source: Fail if code reads DateTime.UtcNow, Time, or Random.Shared directly; inject IClock/IRng. • Static events & memory: Fail if static events or static refs point to scene/domain objects. • Test independence: Fail if unit tests need global setup/teardown or can’t run in parallel. Key insight: If a singleton holds mutable state, it’s a global variable—extract services and inject dependencies.