Skip to main content
Create your own
Lesson illustration

DIP: Depend on Abstractions

Hello! Welcome to our final lesson on the SOLID principles.

In our previous lesson, we explored the Interface Segregation Principle (ISP), learning to create lean, client-specific interfaces. We saw how breaking down "fat" interfaces helps us avoid forcing classes to implement methods they don't need. This skill is the perfect foundation for today's topic.

This lesson completes our study of SOLID by focusing on the Dependency Inversion Principle (DIP). We will address the learning outcome: Apply the Dependency Inversion Principle (DIP) to depend on abstractions, not concretions. We'll see how the well-designed interfaces from ISP become the key to decoupling the core logic of our application from the low-level details, resulting in a system that is flexible, testable, and easier to maintain.

1. Understanding Dependency Inversion

At its core, the Dependency Inversion Principle aims to reverse the traditional flow of dependencies in software. In a conventional design, high-level modules that contain complex business logic often depend directly on lower-level modules that handle details like database access or network communication. This creates a rigid structure where a change in a low-level detail can force changes all the way up to the high-level business rules.

Robert C. Martin defined the principle with two key statements:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).
  2. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

The "inversion" in the name refers to inverting the direction of the dependency arrow. Instead of High-Level -> Low-Level, we get High-Level -> Abstraction <- Low-Level.

Dependency Inversion Principle: Before and After
This diagram shows the shift from a direct dependency on a concrete `SqlDatabase` to a dependency on an abstract `IRepository` interface. This inverts the dependency, as the `SqlDatabase` now depends on (implements) the abstraction defined by the business logic's needs.

To start, let's watch a short video that introduces the principle and its benefits.

Low Level Design 109 | Dependency Inversion Principle | 2022 | System Design

This video from sudoCODE provides a concise introduction to the Dependency Inversion Principle. It will help you understand the formal definition, the problem it solves, and how it leads to more manageable code.

Watch from 01:13 to 04:03. Focus on how the principle is broken down into simpler terms and how the class diagram illustrates the 'inversion' of dependencies by introducing interfaces.

DIP vs. DI and IoC

Given your experience with Spring Boot, you've likely encountered the terms Dependency Injection (DI) and Inversion of Control (IoC). It's crucial to distinguish these from DIP.

  • Dependency Inversion Principle (DIP): A design principle. It's the "D" in SOLID. It states that we should depend on abstractions, not concretions.
  • Inversion of Control (IoC): A broad design paradigm. Instead of your code controlling the program flow and object creation, a framework or container takes over that control.
  • Dependency Injection (DI): A specific pattern to implement IoC. It's the process of providing a component with its dependencies from an external source (the "injector") rather than having the component create them itself.

Think of it this way: you apply the DIP to your design. To make that design work, you use a framework that implements IoC using the DI pattern. Spring's ability to @Autowire an interface and inject a concrete bean at runtime is a perfect example of DI and IoC in action, all made possible by adhering to DIP.

The following article provides a clear distinction.

The Dependency Inversion Principle in Java

The article 'The Dependency Inversion Principle in Java' from Baeldung is excellent for clarifying these related but distinct concepts. Understanding this is key for a developer working with frameworks like Spring.

Read Section 2, 'Dependency Injection and Inversion of Control Are Not DIP Implementations'. This will solidify the relationship between these three terms.

2. A Practical Example: The Coffee Machine

Let's see DIP in action with a practical example. Imagine you have a high-level CoffeeApp that needs to brew coffee. Without DIP, you might write the app to depend directly on a concrete PremiumCoffeeMachine class.

Problem: What if you want to switch to a BasicCoffeeMachine? You'd have to change the CoffeeApp code. The high-level application is tightly coupled to the low-level implementation detail.

Solution: We introduce an abstraction (an interface) that represents the capability our CoffeeApp needs. The CoffeeApp will depend on this interface, and the concrete machine classes will implement it.

The following article walks through this exact refactoring process.

SOLID Design Principles Explained: Dependency Inversion

This article from Stackify uses a simple and effective coffee machine example to demonstrate a step-by-step refactoring that applies DIP.

Read the sections from 'Brewing coffee with the Dependency Inversion Principle' through 'Implementing the coffee machine application'. Observe how the initial concrete classes are refactored to implement CoffeeMachine and EspressoMachine interfaces, and how this decouples the final CoffeeApp.

After the refactoring, the design looks like this:

UML Class Diagram for Coffee Machine illustrating DIP
This UML diagram shows the final, decoupled design. The `BasicCoffeeMachine` implements `CoffeeMachine`, while the `PremiumCoffeeMachine` implements both `CoffeeMachine` and `EspressoMachine`. A higher-level application can now depend on the `CoffeeMachine` interface without knowing which concrete machine is being used.

The CoffeeApp can now be written like this:

public class CoffeeApp {
    private CoffeeMachine coffeeMachine;

    // The dependency is "injected" via the constructor
    public CoffeeApp(CoffeeMachine coffeeMachine) {
     this.coffeeMachine = coffeeMachine;
    }

    public Coffee prepareCoffee() {
        Coffee coffee = this.coffeeMachine.brewFilterCoffee();
        System.out.println("Coffee is ready!");
        return coffee;
    }  
}

This CoffeeApp is completely decoupled from any specific coffee machine. It can work with a BasicCoffeeMachine, a PremiumCoffeeMachine, or any future class that implements the CoffeeMachine interface.

Test your understanding!

Imagine you are building a notification system. You have a high-level OrderService that needs to send a notification when an order is completed. Your first implementation uses a concrete EmailNotifier class directly within OrderService.

public class OrderService {
    private EmailNotifier emailNotifier = new EmailNotifier();

    public void completeOrder(Order order) {
        // ... logic to complete order
        emailNotifier.send("Your order is complete!");
    }
}

How does this design violate DIP? How would you refactor it to add support for an SmsNotifier without modifying OrderService? Describe the interface you would create and how the OrderService would change.

Show answer

This design violates DIP because the high-level module (OrderService) depends directly on a low-level module (EmailNotifier). This makes it rigid.

To fix this, you would introduce an abstraction.

  1. Create an interface: This interface should be owned by the high-level module's layer and represent the functionality it needs.

    public interface Notifier {
        void send(String message);
    }
    
  2. Implement the interface: The low-level details (the concrete notifier classes) now depend on this abstraction.

    public class EmailNotifier implements Notifier {
        @Override
        public void send(String message) { /* ... sends email ... */ }
    }
    
    public class SmsNotifier implements Notifier {
        @Override
        public void send(String message) { /* ... sends SMS ... */ }
    }
    
  3. Refactor the high-level module: The OrderService now depends on the Notifier interface and receives the concrete implementation via dependency injection (e.g., through the constructor).

    public class OrderService {
        private final Notifier notifier;
    
        public OrderService(Notifier notifier) {
            this.notifier = notifier;
        }
    
        public void completeOrder(Order order) {
            // ... logic to complete order
            notifier.send("Your order is complete!");
        }
    }
    

Now, the OrderService is decoupled. You can provide it with an EmailNotifier, an SmsNotifier, or any other Notifier implementation without changing its code.

3. Finding the Right Abstraction

Simply creating an interface with the same methods as a concrete class is not enough. The key to powerful decoupling is defining an abstraction that truly serves the needs of the high-level module, independent of the low-level details.

This means the high-level module effectively owns the interface. It defines the contract it needs, and the low-level modules must conform to it.

Dependency Inversion: What, Why & How? | By Example

This video from the 'About Clean Code' channel provides a deeper look into the philosophy of DIP. It does an excellent job explaining the concept of high-level modules owning the abstraction and the importance of finding the right level of abstraction.

Watch from 04:08 to 09:55. Pay close attention to three key ideas: 1) How the high-level module defines the interface it needs (shifting ownership of the contract). 2) How the Adapter pattern can be used to connect an existing library to your abstraction. 3) The discussion on finding an appropriate, future-proof abstraction, not just a simple interface.

As the video explains, a good abstraction is more than just a Java interface. It's a conceptual model of the interaction that hides irrelevant details. For example, instead of an interface that exposes methods like saveToSqlDatabase(), a better abstraction owned by the business logic might be an interface with a save(Customer customer) method. The detail of how it's saved (SQL, NoSQL, file system) is hidden behind the abstraction.

Conclusion

The Dependency Inversion Principle is the final piece of the SOLID puzzle. It brings together the other principles by using well-defined abstractions (from OCP, LSP, and ISP) to decouple high-level policy from low-level implementation details. Mastering DIP is essential for building the kind of flexible, resilient, and testable systems that are expected in modern software engineering and system design interviews.

Key Takeaways:

  • Decouple Layers: Your high-level business logic should not be tied to implementation details like databases, file systems, or specific third-party libraries.
  • Depend on Abstractions: Both high-level and low-level modules should depend on abstractions (interfaces), not on each other directly.
  • Invert the Dependency: The concrete implementations (details) should depend on the abstractions defined by the high-level modules. The high-level module owns the contract.
  • DIP Enables DI/IoC: DIP is the design principle that makes frameworks like Spring, which use Dependency Injection and Inversion of Control, possible and powerful.

This lesson concludes our module on the SOLID principles. You now have a complete framework for designing classes and their relationships in a way that promotes maintainability and scalability.

In our next module, we will move on to Creational Design Patterns. We'll start with the Singleton pattern and explore standard, reusable solutions for the common problem of object creation.

Can't find a good explanation? Sign up and we'll make it for you

Sign up