The Dependency Inversion Principle (DIP) is a fundamental concept in software development that helps create flexible and maintainable code. In this article, I’ll explore its history, the problem of tight coupling, and different approaches to applying DIP in Swift.

History of the Dependency Inversion Principle
The Dependency Inversion Principle (DIP) was introduced by Robert C. Martin, also known as Uncle Bob, in the 1990s as part of his SOLID principles for object-oriented software design. SOLID is a set of five principles intended to help developers create maintainable, scalable, and testable software. Among these principles, DIP addresses coupling between different layers of an application and promotes flexibility in software architecture.
Origins and Motivation
Before DIP, software was often designed in a way where high-level modules directly depended on low-level modules. This approach made code rigid and difficult to modify because any change in a low-level module could force modifications in high-level logic.
For example, if a high-level module (such as a ViewModel) directly depends on a networking service, switching from a REST API to a GraphQL API or adding caching layers would require significant changes in the ViewModel. This tight coupling made software fragile and difficult to extend.
To solve this, Robert C. Martin formulated DIP based on existing ideas in object-oriented programming and dependency injection. The principle states:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
By introducing abstractions (protocols or interfaces) between high-level and low-level modules, DIP enables software to be more flexible, testable, and reusable.
Evolution in Software Development
The Dependency Inversion Principle has had a significant impact on modern software development:
- Early Object-Oriented Programming (OOP) – Early OOP designs often tightly coupled classes, leading to rigid codebases. DIP helped shift focus to loose coupling.
- Design Patterns – DIP influenced patterns such as Dependency Injection (DI), Inversion of Control (IoC), and Service Locators, which are widely used in software architecture today.
- Modern Frameworks – Popular frameworks like Spring (Java) and ASP.NET Core (C#) embrace DIP principles by providing built-in dependency injection support. In SwiftUI, DIP can be applied manually using protocols and dependency injection techniques, often leveraging Combine for reactive data flow and state management.
- Mobile Development – In iOS and Android development, DIP is commonly used in MVVM and Clean Architecture to separate concerns and improve maintainability.
The Problem: Tight Coupling
Tight coupling occurs when a class strongly depends on another, making changes in one affect others. This happens when a class directly instantiates or relies on a specific implementation instead of an abstraction. Such systems are rigid, hard to maintain, and difficult to test. The Dependency Inversion Principle (DIP) helps reduce tight coupling, making software more modular and flexible.
Consider a situation where a ContentViewModel directly depends on RemoteChapterLoader:
@MainActor
final class ContentViewModel: ObservableObject {
@Published var chapters = [ChapterEntity]()
private let chapterLoader: RemoteChapterLoader
...
init() {
self.chapterLoader = RemoteChapterLoader()
}
...
}
If we illustrate that into and image, it will shown as the circled area :

Why is this a problem?
- Hard to test:
ContentViewModelis tightly coupled toRemoteChapterLoader, making it difficult to replace it with a mock implementation. - Difficult to extend: If we want to add a local database loader, we must modify
ContentViewModel. - Breaks Open/Closed Principle: Any change in
RemoteChapterLoadercould require changes inContentViewModel.
What if We Inject, But Without Inversion?
If we still inject RemoteChapterLoader directly, we get some flexibility but without full inversion:
@MainActor
final class ContentViewModel: ObservableObject {
private let chapterLoader: RemoteChapterLoader
...
init(chapterLoader: RemoteChapterLoader) {
self.chapterLoader = chapterLoader
}
...
}
If we illustrate that into and image, it will shown as the circled area :

Issues:
- The class is still tightly coupled to
RemoteChapterLoader. - Changing the implementation of
chapterLoaderrequires modifyingContentViewModel. - Even though
RemoteChapterLoadercan be injected, what if it is usingfinal, meaning it could not be inherited to be mocked for unit tests?
Solution: Apply Dependency Inversion Principle
The ideal solution if we illustrate the image is the 3rd image, which will be illustrated below :

So, Instead of depending on a concrete class, introducing an abstraction (ChapterLoader protocol) is better:
public protocol ChapterLoader {
func load() async throws -> [ChapterEntity]
}
@MainActor
final class ContentViewModel: ObservableObject {
private let chapterLoader: any ChapterLoader
...
init(chapterLoader: some ChapterLoader) {
self.chapterLoader = chapterLoader
}
...
}
Now we can provide different implementations, such as RemoteChapterLoader or a mock loader for testing:
public actor RemoteChapterLoader: ChapterLoader {
private let client: any HTTPClient
public init(client: some HTTPClient) {
self.client = client
}
public func load() async throws -> [ChapterEntity] {
let data = try await client.get(from: URL(string: "https://a-url.com")!)
return try Self.map(from: data)
}
}
Finally, If we illustrate that into and image, the dependency now is inverted :

Benefits of Dependency Inversion Principle
- Easier Testing: We can provide a mock implementation of ChapterLoader for unit tests.
- Flexibility: Easily switch between different loaders (e.g., LocalChapterLoader, RemoteChapterLoader).
- Improved Maintainability: Changes in one module do not require modifying dependent modules.
- Better Code Reusability: Components become more modular, allowing reuse in different parts of the app.
- Loosely Coupled Design: Reduces tight dependencies, making the system more adaptable to future changes.
- Adheres to SOLID Principles: Supports the Open/Closed Principle, preventing unnecessary modifications to existing code.
- Easier Collaboration: Teams can work independently on different modules, improving development efficiency.
- Future-Proof Code: The system is easier to refactor, ensuring long-term scalability and extensibility.
Conclusion
The Dependency Inversion Principle (DIP) helps build maintainable and scalable iOS applications. By depending on abstractions rather than concrete implementations, we reduce coupling, improve testability, and make our code more flexible for future changes. Applying DIP correctly ensures that our Swift codebase remains clean and adaptable over time.
References
- Martin, Robert C. Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall, 2017.
- Fowler, Martin. Inversion of Control Containers and the Dependency Injection Pattern. 2004. https://martinfowler.com/articles/injection.html
- Essential Developer by Caio and Mike