Last active
February 3, 2026 21:52
-
-
Save rozd/46ebe9e3f9ddd0d2abd526cacaea4ec9 to your computer and use it in GitHub Desktop.
Type-Safe Dependency Injection in Swift: The Composition Root Pattern with KeyPaths. Vapor Example.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // KeyPath-Powered Dependency Injection for Server-Side Swift | |
| // A minimal, standalone example for Vapor applications | |
| // | |
| // GitHub Gist: https://gist.github.com/rozd/swift-composition-root-vapor-example | |
| // Full article: https://medium.com/@rozd/type-safe-dependency-injection-in-swift-the-composition-root-pattern-with-keypaths-385d34f0b202 | |
| import Vapor | |
| // MARK: - Partial (Generic Override Container) | |
| struct Partial<Wrapped> { | |
| private var values: [PartialKeyPath<Wrapped>: Any] = [:] | |
| subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> ValueType? { | |
| get { values[key] as? ValueType } | |
| set { values[key] = newValue } | |
| } | |
| } | |
| // MARK: - Dependencies Protocol | |
| extension Application { | |
| /// Define all application dependencies here. | |
| /// This protocol serves as documentation for what's available. | |
| protocol Dependencies: Sendable { | |
| // Infrastructure | |
| var client: any Client { get } | |
| var logger: Logger { get } | |
| // Example: Database | |
| var database: any Database { get } | |
| // Example: Repository | |
| var userRepository: any UserRepository { get } | |
| // Example: Query Handler | |
| var getUserQueryHandler: GetUserQueryHandler { get } | |
| } | |
| } | |
| // MARK: - Composition Root | |
| extension Application { | |
| struct CompositionRoot: Dependencies { | |
| let app: Application | |
| let resolver = Resolver() | |
| init(app: Application) { | |
| self.app = app | |
| } | |
| // Infrastructure | |
| var client: any Client { | |
| resolver.resolve(\.client) { app.client } | |
| } | |
| var logger: Logger { | |
| resolver.resolve(\.logger) { app.logger } | |
| } | |
| var database: any Database { | |
| resolver.resolve(\.database) { | |
| PostgresDatabase( | |
| configuration: app.config.database, | |
| logger: logger | |
| ) | |
| } | |
| } | |
| // Repositories | |
| var userRepository: any UserRepository { | |
| resolver.resolve(\.userRepository) { | |
| PostgresUserRepository( | |
| database: database, | |
| logger: logger | |
| ) | |
| } | |
| } | |
| // Query Handlers | |
| var getUserQueryHandler: GetUserQueryHandler { | |
| resolver.resolve(\.getUserQueryHandler) { | |
| GetUserQueryHandler( | |
| repository: userRepository, | |
| logger: logger | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| // MARK: - Resolver | |
| extension Application.CompositionRoot { | |
| final class Resolver: @unchecked Sendable { | |
| private let lock = NSRecursiveLock() | |
| private var overridden = Partial<any Application.Dependencies>() | |
| private var persistent: [PartialKeyPath<any Application.Dependencies>: Any] = [:] | |
| /// Resolve a dependency by KeyPath. | |
| /// 1. Check overrides (for testing) | |
| /// 2. Check cache | |
| /// 3. Create via factory and cache | |
| func resolve<T>( | |
| _ keyPath: KeyPath<any Application.Dependencies, T>, | |
| factory: () -> T | |
| ) -> T { | |
| if let overridden = overridden[keyPath] { | |
| return overridden | |
| } | |
| lock.lock() | |
| defer { lock.unlock() } | |
| if let cached = persistent[keyPath] as? T { | |
| return cached | |
| } | |
| let instance = factory() | |
| persistent[keyPath] = instance | |
| return instance | |
| } | |
| /// Override dependencies for testing | |
| func override(_ overrides: Partial<any Application.Dependencies>) { | |
| lock.withLock { overridden = overrides } | |
| } | |
| /// Reset the container (useful between tests) | |
| func reset(overrides: Bool = true, persistent: Bool = true) { | |
| lock.withLock { | |
| if overrides { self.overridden = Partial() } | |
| if persistent { self.persistent = [:] } | |
| } | |
| } | |
| } | |
| } | |
| // MARK: - NSRecursiveLock Extension | |
| private extension NSRecursiveLock { | |
| func withLock<T>(_ block: () throws -> T) rethrows -> T { | |
| lock() | |
| defer { unlock() } | |
| return try block() | |
| } | |
| } | |
| // MARK: - Application Extension | |
| extension Application { | |
| private struct CompositionRootKey: StorageKey { | |
| typealias Value = CompositionRoot | |
| } | |
| /// Access all dependencies via `app.deps` | |
| var deps: any Dependencies { | |
| if let existing = storage[CompositionRootKey.self] { | |
| return existing | |
| } | |
| let root = CompositionRoot(app: self) | |
| storage[CompositionRootKey.self] = root | |
| return root | |
| } | |
| } | |
| // MARK: - Usage Examples | |
| /* | |
| // In routes.swift: | |
| func routes(_ app: Application) throws { | |
| let controller = UserController( | |
| getUserQueryHandler: app.deps.getUserQueryHandler | |
| ) | |
| try app.register(collection: UserRouteV1(controller: controller)) | |
| } | |
| // In a test: | |
| @Test func userQueryReturnsUser() async throws { | |
| var overrides = Partial<any Application.Dependencies>() | |
| overrides[\.userRepository] = MockUserRepository(users: [testUser]) | |
| compositionRoot.resolver.override(overrides) | |
| let user = try await compositionRoot.getUserQueryHandler.handle( | |
| GetUserQuery(userId: testUserId) | |
| ) | |
| #expect(user.name == "Test User") | |
| compositionRoot.resolver.reset() | |
| } | |
| */ | |
| // MARK: - Placeholder Types (replace with your actual implementations) | |
| protocol Database: Sendable {} | |
| protocol UserRepository: Sendable {} | |
| struct PostgresDatabase: Database { | |
| init(configuration: Any, logger: Logger) {} | |
| } | |
| struct PostgresUserRepository: UserRepository { | |
| init(database: any Database, logger: Logger) {} | |
| } | |
| struct GetUserQueryHandler: Sendable { | |
| init(repository: any UserRepository, logger: Logger) {} | |
| } | |
| extension Application { | |
| var config: AppConfig { AppConfig() } | |
| } | |
| struct AppConfig { | |
| var database: Any { () } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment