SwiftQA Random

How do you avoid tight coupling?

What tight coupling actually is, precisely

Two pieces of code are tightly coupled when a change to one forces a change to the other, or when one can’t be understood, tested, or reused without dragging the other along. The goal isn’t zero coupling — it’s making dependencies explicit, minimal, and pointed at abstractions rather than concretions.

1. Depend on protocols, not concrete types — the single most load-bearing technique

// Tightly coupled
final class ProfileViewModel {
    private let repository = NetworkUserRepository()
}

// Loosely coupled
final class ProfileViewModel {
    private let repository: UserRepository
    init(repository: UserRepository) { self.repository = repository }
}

This is what makes testing possible without a live network, and what let Clean Architecture’s inner layers stay framework-agnostic.

2. Dependency injection over singletons/global state

final class ProfileService {
    private let client: HTTPClient
    init(client: HTTPClient) { self.client = client }
}

Constructor injection keeps dependencies visible in the type’s signature.

3. Communicate through narrow interfaces, not shared mutable objects

Passing a whole mutable object around when only one piece of data is needed creates coupling to everything else that object exposes:

// Coupled — SettingsView can read/mutate anything on the whole User object
func showSettings(for user: User) { ... }

// Narrower — only depends on what's actually needed
func showSettings(displayName: String, onSave: @escaping (String) -> Void) { ... }

4. Decouple through events/delegation rather than direct references, where appropriate

final class UploadManager {
    var onProgress: ((Double) -> Void)?  // no coupling to whoever's listening
}

This is the same principle behind Coordinators’ delegate/closure pattern — the screen doesn’t know what happens after it emits an event.

5. Isolate volatile dependencies (third-party SDKs, frameworks) behind your own abstraction

protocol AnalyticsTracking {
    func track(event: String, properties: [String: Any])
}

final class ThirdPartyAnalyticsAdapter: AnalyticsTracking {
    func track(event: String, properties: [String: Any]) {
        ThirdPartySDK.logEvent(event, properties)  // the ONLY place that imports ThirdPartySDK
    }
}

This is a direct application of the Adapter pattern.

6. Module/dependency-graph enforcement (the structural version)

At the scale of modularization/multi-team apps, the same principle gets enforced structurally — SPM’s dependency graph makes illegal coupling a compile error rather than a code-review hope.

7. Favor composition over inheritance

Deep inheritance hierarchies are a classic coupling trap — a subclass is coupled to its entire superclass chain’s implementation details (the fragile base class problem). Preferring protocol composition and injected collaborators keeps dependencies explicit and one level deep rather than an inherited chain of assumptions.

The judgment call — coupling isn’t always bad, and decoupling isn’t free

Introducing a protocol, an injection seam, and a mock for something that has exactly one implementation and will only ever have one is pure overhead with no payoff. The signal that coupling is actually a problem: would I need to change this code because of an unrelated change elsewhere, or would I want to substitute a different implementation (for testing, for a new requirement, for a different platform)? If neither is true, a direct concrete dependency is simply fine.

Interview-ready summary

TechniqueWhat it decouples
Protocol-based dependenciesConsumer from concrete implementation
Dependency injection (constructor)Consumer from global/singleton lifecycle
Narrow interfaces (pass only what’s needed)Consumer from incidental object surface
Events/delegation/closuresEmitter from knowledge of its observers
Adapter around third-party SDKsApp code from vendor API churn
Module dependency graph enforcementStructural — makes illegal coupling a build error
Composition over inheritanceSubclass from superclass implementation fragility

One-liner if pressed: “Tight coupling comes from depending on concrete implementations, global state, or wider interfaces than you actually need — so the fix across every layer, from a single ViewModel to a whole module graph, is the same move: depend on the narrowest protocol/interface that expresses what you actually need, inject it explicitly rather than reaching for a singleton, and enforce that boundary structurally where possible rather than relying on convention — but only where there’s a real reason to substitute or isolate, since abstraction for its own sake is its own form of unnecessary complexity.”