Table of contents
- DIP — High-Level Modules Should Not Be Dragged Around by Low-Level Ones
- What It Means to Invert Dependencies
- DI and DIP — Similar Names, Different Concepts
- DIP in Layered Architecture
- DIP in Practice — How Far Should You Go?
- Key Takeaways
DIP — High-Level Modules Should Not Be Dragged Around by Low-Level Ones
DIP (Dependency Inversion Principle) was proposed by Robert C. Martin and consists of two rules.
- 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.
Terms like “high-level” and “low-level” might sound abstract. Let’s be concrete.
- High-level module: Business policies, core logic. Rules like “create an order and process payment”
- Low-level module: Implementation details. Technical mechanisms like “store in MySQL” or “send mail via SMTP”
When a high-level module directly depends on a low-level module, changes to the low-level affect the high-level. The core business logic gets dragged around by the DB type or the mail library. DIP says to invert this direction of dependency.
What It Means to Invert Dependencies
Before Inversion — The Natural Direction
The most natural structure when first writing code looks like this.
public class OrderService {
private final MySqlOrderRepository repository = new MySqlOrderRepository();
public void createOrder(Order order) {
// Business logic
order.validate();
order.calculateTotal();
// Save
repository.save(order);
}
}
public class MySqlOrderRepository {
public void save(Order order) {
// INSERT into MySQL
String sql = "INSERT INTO orders ...";
// JDBC connection, execution
}
}
Drawing the dependency direction, the high-level (OrderService) directly references the low-level (MySqlOrderRepository).
flowchart TB
subgraph before["Before dependency inversion"]
direction TB
OS["OrderService<br/>High-level — Business logic"] -->|"Direct dependency"| MySQL["MySqlOrderRepository<br/>Low-level — MySQL implementation"]
end
style MySQL fill:#ff6b6b,color:#fff
The problems with this structure are clear. To switch MySQL to PostgreSQL, you must modify OrderService. Testing requires an actual MySQL instance. Business logic is coupled to infrastructure technology.
After Inversion — Depending on Abstractions
Introduce an interface. Then make the high-level module depend on this interface.
public interface OrderRepository {
void save(Order order);
Order findById(Long id);
}
The high-level module depends only on the interface.
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void createOrder(Order order) {
order.validate();
order.calculateTotal();
repository.save(order);
}
}
The low-level module implements the interface.
public class MySqlOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
// INSERT into MySQL
}
@Override
public Order findById(Long id) {
// SELECT from MySQL
return null;
}
}
Drawing the dependency direction again reveals the arrows have changed.
flowchart TB
subgraph after["After dependency inversion"]
direction TB
OS2["OrderService<br/>High-level — Business logic"] -->|"Depends on abstraction"| ORI["OrderRepository<br/>Interface — Abstraction"]
MySQLImpl["MySqlOrderRepository<br/>Low-level — MySQL implementation"] -->|"Implements abstraction"| ORI
end
style ORI fill:#339af0,color:#fff
style OS2 fill:#51cf66,color:#fff
style MySQLImpl fill:#ffd43b,color:#000
OrderService depends on the OrderRepository interface. MySqlOrderRepository also depends on the OrderRepository interface (in the form of implementation). Both depend on the abstraction. Because the low-level module’s dependency direction has flipped toward the high-level module, it’s called “Inversion.”
The Core of Inversion — Who Owns the Interface?
There’s an important point here. Where should the OrderRepository interface reside?
Intuitively, you might think “Why not put it next to the implementation?” — placing it in the same package as the MySQL-related code. But in DIP, the owner of the interface is the side that uses it, i.e., the high-level module.
com.example.order/
├── OrderService.java ← High-level module
├── OrderRepository.java ← Interface (located in high-level package)
└── ...
com.example.order.infrastructure/
├── MySqlOrderRepository.java ← Low-level module (implements the interface)
└── ...
When the interface is in the high-level package, the low-level package depends on the high-level package. Dependencies are inverted at the package level too. This is the complete application of DIP.
flowchart LR
subgraph domain["com.example.order — High-level"]
OS3["OrderService"]
ORI2["OrderRepository<br/>Interface"]
OS3 --> ORI2
end
subgraph infra["com.example.order.infrastructure — Low-level"]
MySQLImpl2["MySqlOrderRepository"]
end
MySQLImpl2 -->|"implements"| ORI2
DI and DIP — Similar Names, Different Concepts
If you’ve used Spring, DI (Dependency Injection) probably comes to mind. DI and DIP are frequently confused due to their similar names, but they are different concepts.
- DIP is a design principle. It provides direction that dependencies should point toward abstractions
- DI is a technique. Objects don’t create their own dependencies; instead, dependencies are injected from outside
DI is a commonly used tool for implementing DIP, but you can use DI without DIP, and you can apply DIP without DI.
DIP Without DI
Here’s an example of applying DIP using the factory pattern. No framework needed.
public class OrderRepositoryFactory {
public static OrderRepository create() {
String dbType = System.getProperty("db.type");
if ("mysql".equals(dbType)) {
return new MySqlOrderRepository();
}
return new PostgresOrderRepository();
}
}
// Usage
OrderRepository repository = OrderRepositoryFactory.create();
OrderService service = new OrderService(repository);
OrderService still depends only on the OrderRepository interface. DIP is maintained. There’s just no DI container.
DI Without DIP
Conversely, you can use DI while still violating DIP.
public class OrderService {
private final MySqlOrderRepository repository;
// Constructor injection — DI is applied
public OrderService(MySqlOrderRepository repository) {
this.repository = repository;
}
}
MySqlOrderRepository is injected from outside, so DI is applied. But since the dependency is on a concrete class rather than an interface, DIP is violated. It’s not the form of injection but the direction of dependency that matters for DIP.
DIP + DI in Spring
In Spring Boot, DIP and DI naturally combine.
Declare the interface.
interface OrderRepository {
fun save(order: Order)
fun findById(id: Long): Order?
}
Annotate the implementation with @Repository.
@Repository
class JpaOrderRepository(
private val jpaRepository: OrderJpaRepository
) : OrderRepository {
override fun save(order: Order) {
jpaRepository.save(order.toEntity())
}
override fun findById(id: Long): Order? {
return jpaRepository.findByIdOrNull(id)?.toDomain()
}
}
The service depends on the interface. Spring finds and injects the implementation.
@Service
class OrderService(
private val orderRepository: OrderRepository // Depends on interface
) {
fun createOrder(order: Order) {
order.validate()
order.calculateTotal()
orderRepository.save(order)
}
}
DIP (depending on the interface) and DI (Spring injecting the implementation) work together. Spring’s IoC container (Inversion of Control) is infrastructure that makes DIP convenient to apply.
DIP in Layered Architecture
In a traditional 3-layer architecture (Presentation -> Business -> Data Access), the dependency direction flows from top to bottom.
flowchart TB
subgraph traditional["Traditional layered structure — Dependencies flow downward"]
Pres["Presentation Layer<br/>Controller"]
Biz["Business Layer<br/>Service"]
Data["Data Access Layer<br/>Repository"]
Pres --> Biz
Biz --> Data
end
In this structure, the business layer depends on the data access layer. Changes to DB technology can affect business logic. Applying DIP means the business layer owns the interfaces, and the data access layer implements them.
flowchart TB
subgraph dip_arch["DIP Applied — Dependencies point inward"]
Pres2["Presentation Layer"]
Biz2["Business Layer<br/>Service + Repository Interfaces"]
Data2["Data Access Layer<br/>Repository Implementations"]
Pres2 --> Biz2
Data2 -->|"implements"| Biz2
end
style Biz2 fill:#339af0,color:#fff
The data access layer’s arrow direction has changed. It now depends toward the business layer. The business layer no longer knows or needs to know which DB is being used. Even switching MySQL to MongoDB doesn’t change a single line in the business layer.
This principle, taken further, leads to hexagonal architecture (Ports & Adapters) and clean architecture. In these architectures, DIP is a core principle. Business logic sits at the center, and external technologies (DB, API, UI) connect like plugins, adapting to the business logic.
DIP in Practice — How Far Should You Go?
Once you understand DIP’s theory, the question arises: “Should I create interfaces everywhere?” The answer is no. Creating interfaces has costs too. Files multiply, code to trace increases, and simple structures can become complex.
Good places to apply DIP include:
- Boundaries with external systems — DB, message queues, external APIs, file systems. Infrastructure technology is likely to change, and you often need to replace them with mocks in tests
- Core business logic — Domain logic should not depend on frameworks or libraries
- Policies likely to change — Discount policies, notification methods, and other parts that change based on business needs
On the other hand, direct dependencies are fine in these cases:
- Stable libraries — Don’t wrap
StringorListfrom the standard library in interfaces - Utilities with virtually no chance of change — Date conversion, string processing, etc.
- When there’s only one implementation — Creating an interface because “it might change someday” is over-engineering. Extracting an interface when the second implementation is actually needed is not too late
Summarizing DIP Application Criteria
// Interface needed — boundary with external systems
interface NotificationSender {
fun send(userId: Long, message: String)
}
class EmailNotificationSender : NotificationSender { /* ... */ }
class SlackNotificationSender : NotificationSender { /* ... */ }
// Interface unnecessary — internal utility with no chance of change
class DateFormatter {
fun format(date: LocalDateTime): String =
date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
}
NotificationSender could change from email to Slack to KakaoTalk. There’s plenty of reason to abstract it behind an interface. DateFormatter just formats dates. If there’s no chance of a second implementation, it’s fine to depend on it directly.
Key Takeaways
| Concept | Description |
|---|---|
| DIP | Both high-level and low-level modules should depend on abstractions |
| Dependency Inversion | Flipping the direction so low-level depends toward high-level |
| Interface Ownership | The interface is owned by the user side (high-level) |
| DI vs DIP | DI is an injection technique, DIP is a design principle. Independent but powerful together |
| Application Criteria | External system boundaries, core domain, policies likely to change |
We’ve covered all five SOLID principles across three parts. SRP limits reasons for change to one, OCP creates extensible structures, LSP ensures polymorphism works correctly, ISP segregates interfaces by client needs, and DIP inverts the direction of dependencies toward abstractions.
These principles shine most when applied together rather than individually. SRP separates responsibilities, OCP creates extension points, LSP maintains polymorphism correctness, ISP tidies interfaces, and DIP establishes dependency direction. Ultimately, they all aim at the same goal — code that is resilient to change.
In the next part, we’ll look at principles beyond SOLID that govern communication between objects. TDA (Tell, Don’t Ask), the Law of Demeter, and CQS (Command-Query Separation) are the main characters.




Loading comments...