Skip to content

Instantly share code, notes, and snippets.

@rozd
Last active February 3, 2026 21:52
Show Gist options
  • Select an option

  • Save rozd/46ebe9e3f9ddd0d2abd526cacaea4ec9 to your computer and use it in GitHub Desktop.

Select an option

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.
// 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