DIP (Dependency Inversion Principle) — the fifth SOLID principle that defines the rules for building dependencies between modules: 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. This principle, described by Robert Martin in Clean Architecture (2017), is the foundation of loosely coupled architecture. According to this book, the dependency inversion principle eliminates rigid couplings between application layers.
Key Takeaways
DIP (Dependency Inversion Principle) is the dependency inversion principle that reverses the traditional view of dependency direction between modules. High-level modules (business logic) should not directly depend on low-level modules (database, network, UI). Instead, both levels depend on abstractions defined in the high-level module.
The formal formulation of DIP includes two rules: A — high-level modules should not depend on low-level modules, both should depend on abstractions. B — abstractions should not depend on details, details should depend on abstractions. The second rule follows from the first: if an abstraction depends on details, it cannot be a stable foundation for a high-level module.
Without DIP, a typical architecture looks like this: BusinessLogic → DatabaseRepository — business logic directly depends on a concrete repository. With DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. BusinessLogic does not know about the existence of DatabaseRepository, it only knows the DatabaseService interface, which is implemented outside business logic.
Inversion means that the control flow and the dependency flow go in opposite directions. Control flow goes top-down: UI → ViewModel → UseCase → Repository. Dependency flow goes bottom-up: Repository implements an interface defined in UseCase. Repository (low-level) depends on UseCase (high-level).
This inversion is the key difference between DIP and ordinary layer separation. In traditional layered architecture, each layer depends on the layer below. In DIP-based architecture, all layers depend on abstractions, while the implementation of these abstractions resides in the infrastructure layer, which is “plugged in” to the upper layers via DI mechanisms.
The DIP mechanism is implemented by defining abstractions in high-level modules and implementing them in low-level modules. The high-level module declares an interface for the functionality it needs. The low-level module implements this interface. Wiring happens at the application's composition root.
The process of introducing DIP into existing code: extract an interface for the low-level module, move this interface to the high-level module (or to a separate abstraction layer), rewrite the high-level module's dependency to use the interface, make the low-level module implement this interface. After these steps, the dependency direction has been reversed.
DIP requires a composition root mechanism — a point in the application where all dependencies are created and wired together. In Android this is Application.get() or the Hilt component, in iOS — AppDelegate or SceneDelegate. The composition root is the only place where the code knows about concrete implementations.
DIP creates architectural boundaries between application layers. When ViewModel depends on the UserRepository interface, a boundary forms between the presentation and domain layers: ViewModel (presentation) does not know where the data comes from. This boundary allows changing the UserRepository implementation (Room → REST → Mock) without affecting the ViewModel. The more such boundaries, the more resilient the application is to framework and library changes.
In the Android architecture recommended by Google, DIP is implemented through UseCases that reside in the domain layer and depend on Repository interfaces. RepositoryImpl are in the data layer and implement these interfaces. The presentation layer (ViewModel) depends on UseCases. The dependency direction goes from presentation to domain, from domain to data — but no layer knows about the concrete implementations of another layer.
DIP and DI are often confused, but they are different concepts. DIP is an architectural principle (WHAT to do: depend on abstractions). DI is an implementation pattern (HOW to do it: pass dependencies via constructor). DIP answers the question “what should modules be based on?”, DI answers “how do objects get their dependencies?”.
Dependency Injection is a way to inject dependencies into an object via constructor, method, or property. When a Kotlin class receives a Repository interface through its constructor — that is DI. The fact that a ViewModel class depends on the Repository interface rather than a concrete RoomRepository implementation — that is DIP. DI is the tool, DIP is the goal.
You can follow DIP without a DI framework: manual wiring of dependencies in the composition root is also DI (manual DI). You can use a DI framework (Dagger, Hilt, Koin) while violating DIP: if a ViewModel directly creates a Repository object via new() — DIP is violated, even if the framework is installed. DIP is an architectural decision, DI is a technical detail.
Let’s look at an Android example of applying DIP to the data layer. Without DIP, a ViewModel directly creates a RoomDatabase and DAO. With DIP — the ViewModel depends on the UserRepository interface, and the concrete RoomUserRepository implementation is provided externally.
// Abstraction belongs to the domain layer (high-level)
interface UserRepository {
fun getUser(id: Int): User
}
// Domain layer depends only on abstraction
class GetUserUseCase(
private val repo: UserRepository
) {
fun execute(id: Int): User = repo.getUser(id)
}
// Implementation in data layer depends on domain layer abstraction
class RoomUserRepository(
private val dao: UserDao
) : UserRepository {
override fun getUser(id: Int): User {
return dao.getById(id)
}
}
// Composition root
class AppModule {
fun provideUserRepository(dao: UserDao): UserRepository {
return RoomUserRepository(dao)
}
}
An iOS example with an Application Coordinator and a navigation protocol:
// Navigation abstraction in domain layer
protocol AuthNavigation {
func navigateToHome()
func navigateToLogin()
}
// ViewModel depends on abstraction, not on UIKit
final class AuthViewModel {
private let navigation: AuthNavigation
init(navigation: AuthNavigation) {
self.navigation = navigation
}
func onLoginSuccess() {
navigation.navigateToHome()
}
}
// Coordinator (UIKit layer) implements domain layer protocol
final class AppCoordinator: AuthNavigation {
func navigateToHome() {
// UIKit navigation code
}
func navigateToLogin() {
// UIKit navigation code
}
}
The key point: AuthViewModel (domain) does not know about the existence of AppCoordinator (UIKit). It only knows the AuthNavigation protocol. If UIKit is replaced by SwiftUI tomorrow — AuthViewModel requires no changes. DIP makes the domain layer independent of UI frameworks and libraries.
Hilt is the standard DI tool for Android, recommended by Google. It is built into Jetpack, supports ViewModel, Fragment, Service, and other Android components. Hilt automates the creation of the composition root through @Module, @Provides, @Inject annotations. Using Hilt does not guarantee DIP compliance — the UserRepository interface must be defined in the domain layer, not in the data layer.
Koin is a lightweight DI framework for Kotlin without code generation or annotation processing. Koin’s DSL (module, single, factory) is easier to learn, but dependency checking happens at runtime rather than at compile time. Koin is popular in multiplatform projects (KMP) due to iOS support.
Dagger 2 is the predecessor of Hilt, still used in large projects. Dagger generates DI code at compile time, providing maximum performance and error diagnostics at build time. Hilt is built on top of Dagger and provides a simplified API. For new projects, Google recommends Hilt as the primary DI framework.
DI modules should correspond to architectural layers and be separated into DomainModule, DataModule, PresentationModule. DomainModule provides only abstractions and UseCases. DataModule provides implementations for abstractions. PresentationModule wires ViewModels with UseCases. This organization ensures that the domain layer remains independent of infrastructure libraries.
When migrating between DI frameworks (e.g., from Koin to Hilt), the DomainModule structure does not change — only the wiring methods in DataModule and PresentationModule change. DIP ensures isolation of domain logic, while the DI framework is a technical wiring mechanism.
Frequently Asked Questions
DIP is necessary at architectural boundaries — between application layers (domain → data, presentation → domain). Within a single layer, DIP may be overkill. For example, a utility class StringFormatter inside the domain layer does not require an interface — if there is no reason to replace it.
No. DIP is a principle: modules should depend on abstractions. DI is a pattern: an object receives dependencies from outside rather than creating them itself. DI is a way to implement DIP, but DIP can be followed without DI (through factories or service locator). DI without DIP is possible but has no architectural value.
Interfaces belong to the module that uses them, not to the module that implements them. UserRepository is declared in the domain layer and implemented in the data layer. This is the key DIP rule: the owner of the abstraction is the consumer, not the provider of the implementation.
DIP makes testing possible on isolated layers. A ViewModel that depends on UserRepository (interface) can be tested with a mock implementation without a database. Without DIP, the ViewModel would depend on RoomUserRepository and require database setup for every test. DIP + DI provide complete module isolation during testing.
Hilt is the standard choice for Android projects, recommended by Google. Koin is an alternative for Kotlin Multiplatform projects. Dagger 2 is for existing projects where migrating to Hilt is not justified. Choosing a framework does not eliminate the need to follow DIP at the architectural level.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also