Using a dispatch_once singleton model in Swift
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
In modern Swift, you do not implement a singleton with dispatch_once. The idiomatic replacement is a static let shared property, which is lazily initialized and thread-safe by language guarantee.
That means the real question today is not "how do I use dispatch_once in Swift?" but "how do I express a singleton correctly in Swift without old Objective-C patterns?"
The Modern Swift Singleton
The standard pattern is:
This gives you:
- one shared instance
- lazy initialization on first use
- thread-safe initialization
- a private initializer that prevents accidental extra instances
That covers the same practical need that dispatch_once used to cover in older Objective-C code.
Why dispatch_once Went Away
Older Cocoa code often looked like this in Objective-C:
Swift does not need that pattern for singleton initialization. Static stored properties already provide the same one-time, thread-safe initialization behavior. Using dispatch_once manually in Swift is not only unnecessary, it also makes the code look dated and less idiomatic.
Keep The Initializer Private
The private initializer is what turns a shared instance into an actual singleton rather than just a convenient global object.
Without the private initializer, any other part of the app could still create more instances and defeat the point of the pattern.
Singleton Does Not Mean Global Free-For-All
A singleton is useful for resources that are genuinely application-wide and logically unique, such as:
- analytics dispatch
- shared configuration cache
- process-wide logging
- app-level coordination services
It is less appropriate for things that should be dependency-injected, scoped per feature, or mocked heavily in tests. The fact that Swift makes singletons easy does not mean every service should become one.
Concurrency Considerations
The singleton instance creation is thread-safe, but the singleton's mutable state is not automatically safe just because the instance itself is shared correctly. If the singleton holds mutable data accessed from multiple tasks or threads, you still need synchronization.
For concurrency-heavy code, an actor can be a better shared singleton-like model:
This preserves the single shared instance idea while giving the mutable state a safer concurrency model.
Testing And Design Tradeoffs
Singletons can make testing harder because they hide dependencies behind global access. A class that silently reads Logger.shared everywhere is harder to substitute in tests than a class that receives a logger through its initializer.
That does not make singletons forbidden. It means they should be used intentionally. If a dependency is logically global and process-wide, a singleton may be fine. If it is merely convenient, dependency injection is often cleaner.
Common Pitfalls
One common mistake is searching for a literal dispatch_once replacement when Swift's static let already solves the initialization problem. Another is assuming thread-safe initialization means thread-safe mutation of all singleton state. Developers also often create singletons for services that would be easier to test and reason about if they were injected explicitly. Finally, if the class is not marked final and the initializer is not private, the singleton model becomes easier to misuse.
Summary
- In modern Swift, use
static let sharedinstead ofdispatch_once. - A private initializer is what makes the shared instance a real singleton.
- Static stored properties are lazily initialized and thread-safe by design.
- Singleton initialization safety does not automatically make mutable state safe.
- Use the pattern sparingly and prefer dependency injection when the dependency is not truly global.

