Singleton Pattern Drawbacks
3 min read
Architecture
| Problem | Why it bites |
|---|---|
| Hidden dependencies | Class doesn't declare what it uses — you grep to find out |
| Hard to test | Can't swap out for a mock; tests share state |
| Lifecycle blur | Lives for the whole process — no clean reset/dispose hook |
| Global mutable state | Two screens fighting over the same instance, order-dependent bugs |
| Concurrency hazards | Easy to write thread-unsafe singletons (less of an issue in Dart, but for async ordering still real) |
These compound: each downside makes the next one worse.
Code in action — the same job, two styles
// ❌ Classic singleton — convenient but problematic
class ApiClient {
ApiClient._();
static final ApiClient instance = ApiClient._();
Future<User> fetchUser() async { ... }
}
// Everywhere in your code:
ApiClient.instance.fetchUser(); // dependency hidden, mock impossible
// ✅ Constructor injection + DI (Riverpod here)
class ApiClient {
ApiClient({required this.baseUrl});
final String baseUrl;
Future<User> fetchUser() async { ... }
}
final apiClientProvider = Provider<ApiClient>(
(ref) => ApiClient(baseUrl: 'https://api.example.com'),
);
// In code
final api = ref.watch(apiClientProvider);
api.fetchUser();
// In tests
ProviderScope(
overrides: [apiClientProvider.overrideWithValue(MockApiClient())],
child: MyApp(),
);
You still have "one instance" semantics — but now the dependency is explicit, swappable, and lifecycle-managed.
When a singleton actually fits
| Situation | Singleton OK? |
|---|---|
| Pure-function utilities with no state | ✅ Or just top-level functions |
| Truly app-wide, immutable config (resolved once) | ✅ |
| Plugins that own a native handle (analytics SDK, logger) | ✅ — but expose via DI for testability |
| Network client, cache, repository | ❌ Use DI — you'll want mocks and scoped instances |
| Anything that holds mutable user state | ❌ Lift to a state manager |
| "Just easier than passing it around" | ❌ Almost always a smell |
Common mistakes to avoid
// ❌ Treating a singleton as inevitable
class Cart {
static final Cart instance = Cart._();
Cart._();
final items = <Item>[];
}
// Two test runs share `items` → flaky tests
// ❌ Singletons with cyclical dependencies
// ApiClient uses AuthService, AuthService uses ApiClient → init order matters
// ✅ DI makes the cycle explicit (or breaks it via a separate token store)
// ❌ Resetting singletons via custom global state — leaks
// ✅ ProviderScope (Riverpod) auto-disposes; get_it has registerSingleton + reset()
// ❌ Singletons that capture BuildContext / Navigator
// → tied to a vanished widget tree, crashes when called later
// ❌ Mixing singleton access with DI in the same codebase
// "Some screens use the DI'd version, others use the singleton" → confusion
// Pick one approach and stick to it
Interview follow-ups
-
Is
get_it'sregisterSingleton"just as bad" as a hand-rolled singleton? It has some of the same drawbacks (global registry, hidden until you grep), but it's a step up: it's typed, replaceable in tests viagetIt.registerSingleton<T>(mock), and disposable viagetIt.reset(). Better than a static-instance class, but not as scoped or compile-safe as Riverpod. -
How do you test code that depends on a true singleton you can't change? You wrap it. Create a thin interface (e.g.,
Analytics), have one implementation forward to the singleton, inject the interface everywhere. In tests, swap in a fakeAnalytics. The singleton remains but is now mockable through the seam. -
Aren't
Provider-style root-scope instances "just singletons"? In effect they're a single instance perProviderScope— but the lifetime is explicit (disposed when the scope is removed), the dependency is declared (ref.watch(provider)), and overrides for tests are first-class. Same uniqueness, none of the downsides. -
When would you prefer a top-level function or class over a singleton? When there's no state.
int hash(String s)doesn't need an instance — top-level function. If your "singleton" has no mutable fields and no I/O setup, you don't need a singleton at all.
How helpful was this content?
Please sign in to rate this article.