Service Locator is an architectural pattern that provides a central registry of services. Client code requests a service through a static locator, without creating it directly or receiving it via constructor. Service Locator is often considered an alternative to Dependency Injection: it is simpler to implement, but hides dependencies and complicates testing. The pattern is implemented through a global Singleton class with service registration and resolution. More details — in the comparison of DI and Service Locator by Martin Fowler.
Key Takeaways
Service Locator is a pattern that centralizes the creation and provision of services. It is based on a Singleton class (Locator) that contains a Registry of services: a dictionary where the key is the service type (or identifier) and the value is the concrete implementation. Client code calls ServiceLocator.resolve(ServiceProtocol.self) and receives a ready-made instance. The pattern does not prescribe how the service is created — factory, DI-container, or new inside the locator.
Pattern structure — Registry (dictionary of type [String: Any]), Locator (static class with register and resolve), Service (the service being registered). The registry can store factories (closures/lambdas for creating objects) or ready-made instances. The locator can be global (one per application) or scoped (per feature/module). Service resolution is a lookup in the dictionary by type. Swift and Kotlin use type as key via metatypes: ObjectIdentifier(ServiceProtocol.self).
| Component | Responsibility | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Global access to services | class ServiceLocator |
| Registry | Storage for factories/instances | [ObjectIdentifier: Any] |
| Service | Concrete implementation | NetworkService() |
History of the pattern — Service Locator was described in the book Java Patterns (1998) and later in Core J2EE Patterns (2001). Martin Fowler's 2004 article compares Service Locator with DI, noting that Service Locator is a "simpler alternative, but worse for testing." In mobile development, Service Locator was used in early Android projects and iOS applications before Dagger and Swinject appeared. Now the pattern is more commonly found in legacy projects and prototypes.
Service Locator in Swift — implementation via static properties and a thread-safe dictionary. A registry with ObjectIdentifier(Protocol.self) as the key and factories (() -> Any) as values is used. Lazy initialization (lazy var) is standard practice: the service is created upon first request. Swift requires explicit type casting during resolve: guard let service = locator.resolve(ServiceProtocol.self) else { return }.
final class ServiceLocator {
static let shared = ServiceLocator()
private var registry: [ObjectIdentifier: Any] = [:]
private let lock = NSLock()
func register<T>(_ type: T.Type, factory: @escaping () -> T) {
lock.lock()
registry[ObjectIdentifier(type)] = factory
lock.unlock()
}
func resolve<T>(_ type: T.Type) -> T {
lock.lock()
defer { lock.unlock() }
guard let factory = registry[ObjectIdentifier(type)] as? () -> T else {
fatalError("Service \(type) not registered")
}
return factory()
}
func reset() {
lock.lock()
registry.removeAll()
lock.unlock()
}
}
// Registration
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }
// Usage
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)
Scope management — the locator can store factories (transient — a new object each time) or ready-made instances (singleton). For factories, a closure is registered and called on each resolve. For singleton, a closure captures the created instance. Adding scopes: .transient, .singleton, .weak (weak reference — the object lives as long as someone holds a reference). Weak scope is convenient for UIKit UIViewControllers to avoid leaks on pop/dismiss.
Service Locator in Kotlin — a compact implementation via object (Singleton) with inline reified functions for type safety. Kotlin allows a concise locator: val service by locator with a delegate, making the code cleaner. Reified generics () replace ObjectIdentifier — the type is obtained from the generic. Kotlin locators often use ConcurrentHashMap for thread safety without explicit locks.
object ServiceLocator {
private val registry = ConcurrentHashMap<Class<*>, () -> Any>()
inline fun <reified T: Any> register(noinline factory: () -> T) {
registry[T::class.java] = factory
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T: Any> resolve(): T {
val factory = registry[T::class.java]
?: throw IllegalStateException("Service ${T::class.simpleName} not registered")
return factory() as T
}
fun clear() {
registry.clear()
}
}
// Registration
ServiceLocator.register<ApiService> { RetrofitApiService() }
// Usage in class
class UserRepository {
private val api: ApiService = ServiceLocator.resolve()
private val db: Database = ServiceLocator.resolve()
}
// Delegate for lazy resolution
class LocatorDelegate<reified T: Any> : Lazy<T> {
override val value: T get() = ServiceLocator.<T>resolve()
override fun isInitialized(): Boolean = true
}
inline fun <reified T: Any> locator(): Lazy<T> = LocatorDelegate()
// Usage: val api by locator()
Service Locator in Android — found in older projects before Dagger. Jetpack Hilt and Koin have replaced Service Locator in the Android community. However, the locator remains relevant for unit tests: a simple ServiceLocator stub with mock services without Hilt. Pro: no need to wait for Dagger compilation for tests. Con: if you forget to override the locator in a test, tests use production services.
Explicitness of dependencies — the main difference. DI declares dependencies explicitly: init(service: ServiceProtocol) — any IDE shows class dependencies. Service Locator hides them: dependencies = ServiceLocator.resolve() — hidden inside the method. With DI, you can immediately see all class dependencies; with Service Locator, you need to read the entire class body. This makes Service Locator code less predictable: changing the registry can break any class that uses the locator.
| Characteristic | Service Locator | Dependency Injection |
|---|---|---|
| Dependency visibility | Hidden inside method bodies | Explicit in constructor |
| Testing | Configure global registry | Mock in constructor |
| Modularity | Global registry — not modular | Modules with separate containers |
| Complexity | Simple implementation, 50-100 lines | Requires Dagger/Swinject |
| Time-to-market | Quick start | Container setup required |
When Service Locator is justified — prototypes and MVPs (quick start without configuration). Legacy projects where adding a DI framework is impossible (complex build, linter restrictions). Instrumentation libraries (logging, crash reporting) — they are already global. For production applications with a team of 3+ developers, DI is recommended: explicit dependencies reduce the number of errors during refactoring and simplify onboarding new developers.
Hidden dependencies — a class that uses ServiceLocator.resolve() inside a method cannot be analyzed statically. The IDE does not show dependencies, the compiler does not check whether the service is registered. The "Service not registered" error occurs only at runtime. Refactoring becomes dangerous: removing a service from the registry can break any class in the application. DI solves this problem through compile-time checks (Dagger) or explicit constructors.
Testing problem — each test must configure ServiceLocator.shared with all dependencies. After the test — reset the state. During parallel test execution, the global state of ServiceLocator.shared leads to race conditions: one test registers a mock, another test receives someone else's mock. Solution: scoped locators (one per test) or ThreadLocal. DI solves this problem from the start: each test creates its own instance with mock dependencies.
// Problem testing Service Locator
class LoginViewModelTests: XCTestCase {
override func setUp() {
super.setUp()
// Setting up global registry for test
ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
}
override func tearDown() {
ServiceLocator.shared.reset()
super.tearDown()
}
func testLogin() {
let viewModel = LoginViewModel() // uses ServiceLocator internally
// test...
}
}
Alternatives to Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — a simple pattern without global state: a factory class creates services and is passed via constructor. Ambient Context — a thread-safe alternative for cross-cutting concerns (logging, authorization). The best alternative is Constructor Injection with a manual factory without a DI framework: explicit creation of dependencies in a factory with constructor passing gives DI clarity without the complexity of setting up Dagger/Swinject.
Frequently Asked Questions
Many developers consider Service Locator an anti-pattern because it hides dependencies, complicates testing, and creates hidden coupling between classes. However, in prototypes, small projects, and for global services (logging, analytics), Service Locator can be justified. The decision depends on context: for a production application with a team — DI, for a solo developer on a prototype — Service Locator.
A DI container (Dagger, Swinject) injects dependencies into an object automatically — the object does not know about the container's existence. Service Locator — the object itself requests dependencies from the registry. A DI container follows the IoC principle; Service Locator violates it: the object manages the retrieval of its own dependencies. A DI container works before object creation (via constructor), Service Locator — anywhere in the code.
Service Locator is justified in iOS for: global services (Analytics, Logger, Crashlytics), prototypes where setting up Swinject is overkill, and for unit tests of large legacy modules. For new iOS projects, Swinject or manual DI via constructor is recommended. SwiftUI with @Environment — also a form of DI, avoiding Service Locator.
Use a thread-safe collection (NSLock in Swift, ConcurrentHashMap in Kotlin). For tests — ThreadLocal or scoped locator. Alternative: async-local storage — services tied to a coroutine/actor. The best solution is to avoid Service Locator for parallel tests and use DI with explicit object creation for each test.
Service Locator is typically implemented as a Singleton, but this is not required. You can create a locator instance for a module (feature-scoped locator) and pass it via constructor. Feature-scoped locator solves the global state problem but does not solve the hidden dependencies problem. This pattern is called Ambient Context or Scoped Locator.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also