Skip to content

WeaveDI Best Practices

Recommended patterns and practices for using WeaveDI effectively in production applications.

Property Wrapper Selection

Use @Injected for Most Cases (v3.2.0+)

swift
// ✅ Recommended: Type-safe, TCA-style
@Injected(\.userService) var userService
@Injected(\.apiClient) var apiClient

Why:

  • Compile-time type safety with KeyPath
  • Non-optional by default (liveValue/testValue fallback)
  • Better testing support with withInjectedValues
  • TCA-compatible API

Use @Factory for Fresh Instances

swift
// ✅ Good: Stateless operations
@Factory var pdfGenerator: PDFGenerator
@Factory var reportBuilder: ReportBuilder
@Factory var dateFormatter: DateFormatter

When to use:

  • Stateless services (PDF generators, formatters, parsers)
  • Each operation needs isolated state
  • Concurrent processing with independent instances
  • Builder patterns requiring fresh instances

Example:

swift
class DocumentService {
    @Factory var pdfGenerator: PDFGenerator

    func generateReports(data: [ReportData]) async {
        await withTaskGroup(of: PDF.self) { group in
            for item in data {
                group.addTask {
                    // Each task gets fresh generator - no state conflicts
                    let generator = self.pdfGenerator
                    return generator.generate(item)
                }
            }
        }
    }
}

Avoid @Injected/@SafeInject (Deprecated v3.2.0)

swift
// ❌ Avoid: Deprecated
@Injected var service: UserService?
@SafeInject var api: APIClient?

// ✅ Use instead:
@Injected(\.service) var service
@Injected(\.api) var api

Dependency Organization

Group Dependencies by Feature

swift
// ✅ Good: Feature-based organization
// File: DI/UserFeatureDependencies.swift
extension InjectedValues {
    // User feature dependencies
    var userService: UserService {
        get { self[UserServiceKey.self] }
        set { self[UserServiceKey.self] = newValue }
    }

    var userRepository: UserRepository {
        get { self[UserRepositoryKey.self] }
        set { self[UserRepositoryKey.self] = newValue }
    }
}

// File: DI/AuthFeatureDependencies.swift
extension InjectedValues {
    // Auth feature dependencies
    var authService: AuthService {
        get { self[AuthServiceKey.self] }
        set { self[AuthServiceKey.self] = newValue }
    }

    var tokenManager: TokenManager {
        get { self[TokenManagerKey.self] }
        set { self[TokenManagerKey.self] = newValue }
    }
}

Benefits:

  • Clear feature boundaries
  • Easy to find related dependencies
  • Easier to remove features
  • Better code organization

Centralize InjectedKey Definitions

swift
// ✅ Good: All keys in one place
// File: DI/InjectedKeys.swift
struct UserServiceKey: InjectedKey {
    static var liveValue: UserService = UserServiceImpl()
    static var testValue: UserService = MockUserService()
}

struct APIClientKey: InjectedKey {
    static var liveValue: APIClient = URLSessionAPIClient()
    static var testValue: APIClient = MockAPIClient()
}

// Then extend InjectedValues in feature files

Scope Management

Use Appropriate Scopes

swift
// ✅ Singleton for app-wide services
struct LoggerKey: InjectedKey {
    static var liveValue: Logger = ConsoleLogger()  // Shared instance
}

// ✅ Scoped for feature-specific services
await WeaveDI.Container.bootstrap { container in
    container.register(SessionService.self, scope: .session) {
        SessionServiceImpl()
    }
}

Scope Guidelines:

ScopeUse CaseExample
SingletonApp-wide servicesLogger, Analytics, Config
SessionUser session servicesAuth token, User preferences
RequestPer-request servicesAPI client per network call
TransientFresh instancesFormatters, Builders

Example: Multi-Scope Architecture

swift
// App-wide singleton
struct AnalyticsKey: InjectedKey {
    static var liveValue: Analytics = FirebaseAnalytics()
}

// Session-scoped service
class SessionManager {
    @Injected(\.authToken) var authToken  // Changes per session
    @Injected(\.analytics) var analytics  // Shared singleton

    func login(credentials: Credentials) async {
        // authToken is session-specific
        // analytics is app-wide
    }
}

Performance Optimization

Minimize Dependency Count

swift
// ❌ Bad: Too many dependencies
class ViewModel {
    @Injected(\.service1) var service1
    @Injected(\.service2) var service2
    @Injected(\.service3) var service3
    @Injected(\.service4) var service4
    @Injected(\.service5) var service5  // Too many!
}

// ✅ Good: Compose services
class ViewModel {
    @Injected(\.userFacade) var userFacade  // Facade pattern
}

// Facade combines related services
class UserFacade {
    @Injected(\.userService) var userService
    @Injected(\.authService) var authService
    @Injected(\.profileService) var profileService

    func performUserAction() {
        // Coordinate multiple services
    }
}

Lazy Loading for Heavy Dependencies

swift
// ✅ Good: Lazy initialization
struct DatabaseKey: InjectedKey {
    static var liveValue: Database {
        // Expensive initialization deferred
        Database.shared
    }
}

// Access only when needed
class DataService {
    @Injected(\.database) var database

    func saveData() {
        // Database initialized only when first accessed
        database.save()
    }
}

Use @Factory for Concurrent Operations

swift
// ✅ Good: Parallel processing with @Factory
class ImageProcessor {
    @Factory var imageFilter: ImageFilter

    func processImages(_ images: [UIImage]) async -> [UIImage] {
        await withTaskGroup(of: UIImage.self) { group in
            for image in images {
                group.addTask {
                    // Fresh filter for each image - no thread conflicts
                    let filter = self.imageFilter
                    return filter.apply(to: image)
                }
            }

            var results: [UIImage] = []
            for await result in group {
                results.append(result)
            }
            return results
        }
    }
}

Testing Strategies

Use withInjectedValues for Tests

swift
// ✅ Good: Scoped dependency override
func testUserLogin() async {
    await withInjectedValues { values in
        values.authService = MockAuthService()
        values.userService = MockUserService()
    } operation: {
        let viewModel = LoginViewModel()
        await viewModel.login(credentials: testCredentials)

        XCTAssertTrue(viewModel.isLoggedIn)
    }
}

Benefits:

  • Automatic cleanup after test
  • No global state pollution
  • Type-safe value assignment
  • Works with async/await

Define Test Values in InjectedKey

swift
// ✅ Good: Built-in test values
struct UserServiceKey: InjectedKey {
    static var liveValue: UserService = UserServiceImpl()
    static var testValue: UserService = MockUserService()  // Predefined mock
}

// Tests use testValue automatically in test context
func testWithDefaults() async {
    await withInjectedValues { values in
        // testValue used automatically
    } operation: {
        // Test code
    }
}

Create Test Helpers

swift
// ✅ Good: Reusable test setup
extension XCTestCase {
    func withTestDependencies(
        userService: UserService = MockUserService(),
        apiClient: APIClient = MockAPIClient(),
        operation: () async throws -> Void
    ) async rethrows {
        await withInjectedValues { values in
            values.userService = userService
            values.apiClient = apiClient
        } operation: {
            try await operation()
        }
    }
}

// Use in tests
func testExample() async throws {
    await withTestDependencies {
        // Test with standard mocks
    }
}

Error Handling

Handle Missing Dependencies Gracefully

swift
// ✅ Good: Fallback values
struct LoggerKey: InjectedKey {
    static var liveValue: Logger = ConsoleLogger()
    static var testValue: Logger = NoOpLogger()  // Silent in tests
}

// Service always has a logger, even if not configured
class Service {
    @Injected(\.logger) var logger  // Never nil

    func performAction() {
        logger.log("Action performed")  // Safe to call
    }
}

Validate Critical Dependencies at Startup

swift
// ✅ Good: Early validation
@main
struct MyApp: App {
    init() {
        validateDependencies()
        setupDependencies()
    }

    func validateDependencies() {
        // Check critical dependencies exist
        precondition(
            type(of: InjectedValues.current.apiClient) != Never.self,
            "API Client must be configured"
        )
    }

    func setupDependencies() {
        // Configure dependencies
    }
}

Provide Meaningful Error Messages

swift
// ✅ Good: Descriptive errors
struct APIClientKey: InjectedKey {
    static var liveValue: APIClient {
        guard let baseURL = ProcessInfo.processInfo.environment["API_BASE_URL"] else {
            fatalError("""
                ❌ API_BASE_URL environment variable not set

                Please configure API_BASE_URL in your scheme's environment variables:
                1. Edit Scheme → Run → Arguments → Environment Variables
                2. Add: API_BASE_URL = https://api.example.com
                """)
        }
        return URLSessionAPIClient(baseURL: baseURL)
    }
}

Architecture Patterns

Use Protocol-Based Design

swift
// ✅ Good: Protocol for abstraction
protocol UserService {
    func fetchUser(id: String) async throws -> User
    func updateUser(_ user: User) async throws
}

// Multiple implementations possible
class ProductionUserService: UserService { /* ... */ }
class MockUserService: UserService { /* ... */ }
class CachedUserService: UserService { /* ... */ }

// Define once, swap implementations
struct UserServiceKey: InjectedKey {
    static var liveValue: UserService = ProductionUserService()
    static var testValue: UserService = MockUserService()
}

Layer Your Dependencies

swift
// ✅ Good: Clear layers
// Layer 1: Infrastructure (bottom)
extension InjectedValues {
    var networkClient: NetworkClient { /* ... */ }
    var database: Database { /* ... */ }
    var logger: Logger { /* ... */ }
}

// Layer 2: Data/Repository
extension InjectedValues {
    var userRepository: UserRepository { /* ... */ }
    var productRepository: ProductRepository { /* ... */ }
}

// Layer 3: Domain/Business Logic
extension InjectedValues {
    var userService: UserService { /* ... */ }
    var orderService: OrderService { /* ... */ }
}

// Layer 4: Presentation
// ViewModels inject services from layer 3

Avoid Circular Dependencies

swift
// ❌ Bad: Circular dependency
class ServiceA {
    @Injected(\.serviceB) var serviceB
}

class ServiceB {
    @Injected(\.serviceA) var serviceA  // Circular!
}

// ✅ Good: Introduce abstraction
protocol EventBus {
    func publish(_ event: Event)
}

class ServiceA {
    @Injected(\.eventBus) var eventBus  // Both depend on abstraction
}

class ServiceB {
    @Injected(\.eventBus) var eventBus  // No circular dependency
}

Use Composition Over Inheritance

swift
// ❌ Bad: Inheritance-based
class BaseService {
    @Injected(\.logger) var logger
}

class UserService: BaseService {
    // Inherits logger
}

// ✅ Good: Composition-based
class UserService {
    @Injected(\.logger) var logger  // Explicit
    @Injected(\.database) var database

    // Clear, self-contained
}

Code Organization Checklist

  • [ ] Use @Injected for new code (v3.2.0+)
  • [ ] Group dependencies by feature/module
  • [ ] Define clear dependency layers
  • [ ] Minimize dependencies per class (< 5)
  • [ ] Use protocols for abstractions
  • [ ] Provide both liveValue and testValue in InjectedKey
  • [ ] Validate critical dependencies at startup
  • [ ] Document dependency relationships
  • [ ] Avoid circular dependencies
  • [ ] Use appropriate scopes for services

Anti-Patterns to Avoid

❌ Service Locator Pattern

swift
// ❌ Bad: Manual service location
class ViewModel {
    func loadData() {
        let service = InjectedValues.current.userService  // Bad!
        // Use service
    }
}

// ✅ Good: Inject dependencies
class ViewModel {
    @Injected(\.userService) var userService

    func loadData() {
        // Use userService
    }
}

❌ Global Singletons

swift
// ❌ Bad: Global singleton
class APIClient {
    static let shared = APIClient()
}

// ✅ Good: Managed by DI
struct APIClientKey: InjectedKey {
    static var liveValue: APIClient = APIClient()
}

// Inject where needed
@Injected(\.apiClient) var apiClient

❌ Constructor Injection with Defaults

swift
// ❌ Bad: Hidden dependencies
class UserService {
    init(
        apiClient: APIClient = InjectedValues.current.apiClient,  // Bad!
        database: Database = InjectedValues.current.database
    ) { }
}

// ✅ Good: Explicit dependency injection
class UserService {
    @Injected(\.apiClient) var apiClient
    @Injected(\.database) var database

    init() { }  // Clean initializer
}

Runtime Diagnostics Control

  • WEAVEDI_REGISTRY_HEALTH_LOGGING=1: opt in if you want auto health-check/auto-fix logs (default is off).
  • WEAVEDI_REGISTRY_AUTO_HEALTH=0: disable the UnifiedRegistry health-check loop entirely.
  • WEAVEDI_REGISTRY_AUTO_FIX=0: avoid triggering automatic fixes when the score drops.

All flags are consumed by WeaveDIConfiguration.applyFromEnvironment(), so you can toggle them per scheme or CI job.

Log levels for runtime diagnostics

Tune noise without touching env vars by calling UnifiedDI.setLogLevel(_:):

LevelEmits
.allevery diagnostic message (default)
.registrationregistration successes + critical errors
.optimizationoptimization lifecycle + errors
.errorserrors plus registration failures/overwrites
.offsilent mode
swift
WeaveDIConfiguration.applyFromEnvironment()
UnifiedDI.setLogLevel(.errors)

Registry health/auto-fix logs stay disabled unless you explicitly enable WEAVEDI_REGISTRY_HEALTH_LOGGING.

Next Steps

CI Diagnostics Automation

Catch duplicate providers/scope drifts

Add WeaveDITools diagnose-components --json to your CI pipeline to block duplicate providers or scope mismatches before the build stage. The deploy.yml example:

yaml
- name: 🧩 Component diagnostics
  run: |
    swift run --configuration release WeaveDITools diagnose-components --json > component-report.json
    python3 - <<'PY'
    import json, sys
    data = json.load(open('component-report.json'))
    issues = data.get('issues', [])
    if issues:
        for item in issues:
            print(f"- {item['type']}: {', '.join(item['providers'])} -> {item.get('detail')}")
        sys.exit(1)
    PY

Metadata-powered cycle hints

The new check-cycles subcommand inspects compile-time component metadata to surface simple component cycles:

bash
swift run WeaveDITools check-cycles --json

For CI, call the bundled Scripts/check-component-cycles.sh helper. It exits with code 1 when a cycle is found, enriching your “compile-time hint channel” with actionable failures.

Released under the MIT License.