ReviseAlgo Logo

SOLID Principles

Dependency Inversion Principle

High-level modules should depend on abstractions, not concrete details

Last Updated: June 26, 2026 23 min read

The Dependency Inversion Principle (DIP) is the final principle of SOLID. It states two key rules: "High-level modules should not depend on low-level modules. Both should depend on abstractions." and "Abstractions should not depend on details. Details should depend on abstractions." DIP shifts the control flow, decoupling core business logic from peripheral concerns (like databases or notification clients) to enable modular, testable, and maintainable systems.

1. Learning Objectives

  • Define the Dependency Inversion Principle and describe its two core tenets.
  • Distinguish between Dependency Inversion (DIP), Dependency Injection (DI), and Inversion of Control (IoC).
  • Analyze how direct coupling between high-level policy and low-level detail violates DIP.
  • Implement loose coupling with constructor injection in Java, Python, and C++.
  • Trace how dynamic method dispatch and runtime reflection resolve abstract dependencies.

2. Problem Statement

In traditional procedural and object-oriented design architectures, high-level policy modules depend directly on low-level utility modules. For example, a high-level NotificationService might instantiate a concrete EmailSender directly inside its constructor to dispatch alerts.

This creates severe coupling:

  • Rigidity: If the business decides to send SMS alerts instead of emails, the core NotificationService class must be modified, violating the Open/Closed Principle.
  • Untestability: Because the NotificationService instantiates the real EmailSender internally (which connects to external SMTP servers), it is impossible to unit test the service in isolation without actually sending real emails.
  • Cascading Changes: Changes to low-level implementation details (such as updating connection timeouts or SMTP library versions) trigger recompilations and redeployments of the high-level business policy.

3. Real-world Analogy

Think of Electrical Wall Outlets vs. Hardwired Appliances:

  • The Violation: If you hardwired your bedside lamp directly into the copper wiring of your house's electrical system, you would have an extremely coupled setup. To replace the lamp with a fan, you would need to cut the wires, hire an electrician, and splice new connections. The lamp (detail) depends directly on the house's electrical circuit (high-level power provider).
  • The Fix: The house defines a standard plug socket (the abstraction). The lamp now has a plug (the detail). Both the electrical grid and the lamp conform to the plug socket standard. This inversion allows you to plug in a lamp, a phone charger, or a vacuum cleaner without altering the house's wiring.

4. Theory (DIP vs. DI vs. IoC)

Understanding the distinction between these three design concepts is essential:

  • Dependency Inversion Principle (DIP): A high-level design guideline that instructs us to decouple modules by ensuring that both high-level and low-level code depend on abstractions (interfaces or abstract classes).
  • Dependency Injection (DI): A structural design pattern used to implement DIP. It is the practice of passing (injecting) a dependency into a class (usually via constructor, setter, or field) rather than allowing the class to instantiate the dependency itself.
  • Inversion of Control (IoC): A broader architectural principle where the control flow of a program is inverted. Instead of your custom application code calling a framework library, the framework library manages the lifecycle of classes and calls your code (e.g., Spring framework managing object life cycles).

5. Visual Diagrams (Before vs. After DIP structures)

Before: Direct Coupling (Violates DIP)

The high-level NotificationService depends directly on concrete, low-level modules, creating rigid coupling:

NotificationService (High-level)
Instantiates concrete EmailSender internally:
sender = new EmailSender();
▼ depends directly on
EmailSender (Low-level)
+ sendEmail(to, body)

After: Inverted Abstraction Layer (DIP Compliant)

Both modules now depend on a shared abstraction, reversing the dependency flow:

NotificationService (High-Level)
Accepts abstract MessageSender in constructor
▼ depends on abstraction
«interface» MessageSender
+ sendMessage(to, body)
▲ implements
EmailSender
+ sendMessage(to, body)
▲ implements
SmsSender
+ sendMessage(to, body)

6. Syntax Explanation

Decoupling is achieved by referencing abstractions in class members and injecting concrete objects at construction time:

  • Java: We use final instance fields (e.g. private final MessageSender sender;) and request it via constructor argument. This ensures that dependency references are immutable and initialized immediately at runtime.
  • Python: Python uses constructors (__init__) with type annotation annotations. We inject objects conforming to Abstract Base Classes (ABCs) or standard classes.
  • C++: We avoid raw pointers or static declarations. We use smart pointers (e.g. std::shared_ptr<MessageSender> or std::unique_ptr<MessageSender>) to manage dependencies dynamically without manually managing allocation lifecycles.

7. Step-by-Step Implementation

  • Step 1: Identify instances where a class uses the new keyword to instantiate a helper component or utility module inside its constructor.
  • Step 2: Abstract the helper component's methods into a new interface (e.g., MessageSender).
  • Step 3: Make the helper component implement this new interface.
  • Step 4: Modify the client class constructor to accept the interface as a parameter, saving it in a private field.
  • Step 5: When instantiating the classes in the main thread (or using an IoC framework), instantiate the concrete helper and inject it into the client constructor.

8. Complete Code (Mini Project)

9. Code Walkthrough

Let's review the structure of the refactored code:

  • In the bad design, the NotificationServiceViolation class directly instantiates ConcreteEmailSender using the new keyword in its constructor. This forces a direct dependency on email sending hardware and blocks test mock injections.
  • In the refactored design, we define the interface MessageSender containing the abstract operation sendMessage(to, body).
  • Both EmailSender and SmsSender implement this interface, adapting the capability for their respective protocols.
  • NotificationService defines its dependency as a reference to the MessageSender abstraction. It does not instantiate any concrete sender internally.
  • During startup execution, the concrete sender is passed into the service constructor. The service relies purely on the abstraction contract, completely isolating business logic from sending detail changes.

10. Execution Flow

  1. Initialization: The application container or orchestrator instantiates EmailSender on the heap.
  2. Dependency Injection: The orchestrator instantiates NotificationService, passing the email sender reference into its constructor.
  3. Trigger Alert: Client invokes sendAlert("admin@site.com", "Error").
  4. Dynamic dispatch: The service delegates the invocation to the abstract method pointer (e.g. sender.sendMessage(...)). The runtime maps this lookup to the concrete EmailSender.sendMessage() call, completing execution.

11. Internal Working (JVM & Reflection)

At runtime, advanced frameworks automate dependency injection using metadata reflection and object graphs:

  • Object Graph Assembly: When a framework (like Spring) starts up, it scans classes annotated with configuration metadata (e.g. @Component, @Autowired in Java). It identifies dependency requirements for each class constructor and maps them out in a directed acyclic graph (DAG).
  • Dynamic Reflection Injection: If constructor fields are not accessible, the framework uses Java Reflection (Field.setAccessible(true)) or class constructors to instantiate and inject instances directly on the heap.
  • Late Binding: At the JVM level, methods invoked via interfaces correspond to the invokeinterface bytecode instruction. Rather than compilation-linked jump offsets, the JVM performs runtime vtable lookups to direct execution to the target subclass instructions.

12. Complexity Analysis

  • Time Complexity: $O(1)$ lookup overhead during startup (resolving dependencies via reflection) and standard polymorphic dispatch during execution.
  • Space Complexity: $O(N)$ heap references to store dependency variables, where $N$ is the number of component linkages. This has negligible impact on standard server memory configurations.

13. Best Practices

  • Program to interfaces: Always declare field members and function arguments as interface types rather than concrete types.
  • Use constructor injection for mandatory dependencies: This guarantees the class is instantiated in a valid, functional state. Use setter injection only for optional dependencies.
  • Avoid Service Locators: Do not query a locator class (e.g. ServiceLocator.get(EmailSender.class)) to retrieve dependencies. This conceals class coupling instead of resolving it.
  • Keep abstraction layers cohesive: Ensure your interfaces represent broad, reusable capabilities rather than mirroring a single concrete class.

14. Common Mistakes

  • Instantiating dependencies inside classes: Writing code containing new ConcreteHelper() in class methods or constructors.
  • Depending on concrete data models: While business services should depend on abstractions, it is acceptable to instantiate lightweight value objects (DTOs, entity models, strings) directly since they carry state rather than behavior.
  • Leaky Abstractions: Creating interfaces that expose implementation details (e.g. declaring a database query method that throws a SQL-specific exception, exposing SQL parameters on a file system interface).

15. Interview Questions

Q: What is the main difference between DIP and Dependency Injection (DI)?
Answer: DIP is a high-level design principle advising that code should depend on abstractions. DI is a implementation pattern (constructor, setter, or field injection) used to fulfill DIP by passing dependencies into objects.
Q: What is Inversion of Control (IoC)?
Answer: IoC is a broad architectural paradigm where control flow is inverted. In custom procedural code, you call library methods. In IoC, a framework container manages life cycles and calls your code callbacks when events occur (e.g., servlet containers executing custom route handlers).
Q: Is it an LSP or DIP violation to instantiate a String object inside a class?
Answer: No. Instantiating simple data transfer objects, immutable structures (like Java String or custom entities), or helper data structures (like List or Map) does not violate DIP. DIP applies to dependencies that contain business logic or interface boundaries (e.g., database clients, network brokers, repository layers).

16. Practice Exercises

  • Easy: Refactor a Car class that instantiates a concrete V8Engine in its constructor so it can accept a general Engine interface.
  • Medium: Refactor a high-level OrderProcessor class that logs transaction events directly to a concrete LocalFileLogger class. Segregate logging with an interface so logs can be directed to databases or cloud brokers.
  • Hard: Design an image processing application that applies filters to images. Introduce dependency inversion between the main processor engine and the specific image formats (e.g., JPEG, PNG, RAW) and filter algorithms (e.g., blur, sharpen), ensuring new filters can be added via plugin configurations.

17. Challenge Problem

Design a mockable cloud payment processing system. The core checkout workflow requires validating inventory, checking billing details, applying local tax calculations, and executing bank wire transfers. Design a structure using Dependency Inversion where the CheckoutService depends strictly on abstract interfaces. Write complete classes showing how you would inject a SandboxPaymentGateway during test execution versus a StripePaymentGateway in production, demonstrating zero code modifications to the core CheckoutService class.

18. Summary

  • DIP states high-level policies should not depend on low-level details. Both must depend on abstractions.
  • Tight coupling forces cascading modifications, rigid architectures, and blocks unit testing.
  • Dependency Injection implements DIP by passing interface implementations via constructors.
  • Inversion of Control framework containers (like Spring) automate dependency resolution at runtime.

19. Cheat Sheet

SOLID Principle Core Focus Violation Symptom Refactoring Resolution
SRP Cohesive class change reasons God classes, massive files Extract delegate classes
OCP Extend without code modifications Switch-case checking matching enum types Polymorphic dynamic dispatch
LSP Subclass substitutability UnsupportedException thrown in subclasses Extract interfaces, prefer composition
ISP Client-specific thin interfaces Empty overrides, bloated interface contracts Decompose into multiple role interfaces
DIP Abstractions decouple details Direct concretion instantiations inside code constructors Constructor Dependency Injection

20. Quiz

1. Which of the following defines the Dependency Inversion Principle?

A) Subclasses must be substitutable for parent classes
B) High-level modules should not depend on low-level modules; both should depend on abstractions (Correct)
C) Software components should be open for extension but closed for modification

2. What does DIP state about abstractions and details?

A) Abstractions should depend on details
B) Abstractions should not depend on details; details should depend on abstractions (Correct)
C) Abstractions and details should be combined in a single class

3. How is Dependency Injection related to Dependency Inversion?

A) DI is a technique used to implement the design principle of DIP (Correct)
B) They are identical concepts at all levels
C) DI is a compiler configuration while DIP is a database structure

4. What is a key benefit of Dependency Inversion?

A) It makes code execute faster in JIT compilation
B) It decouples high-level policy from low-level utility, improving modularity and unit testability (Correct)
C) It removes the need for virtual destructors in C++

5. Which pattern is considered a violation of DIP?

A) Instantiating business dependencies directly inside constructors using the "new" keyword (Correct)
B) Declaring class fields as final interface references
C) Passing argument interfaces to methods

6. What is the Service Locator pattern's relationship to DIP?

A) It is the recommended way to implement DIP
B) It is often considered an anti-pattern because it hides dependencies rather than injecting them (Correct)
C) It compiles dependencies into binary files

7. What is "Inversion of Control" (IoC)?

A) Inverting method return types in subclass overrides
B) Reversing control flow so a framework manages lifecycles and calls custom code (Correct)
C) Forcing database engines to call client routes

8. Is instantiating a String or ArrayList class inside a service constructor a DIP violation?

A) Yes, all class instantiations violate DIP
B) No, instantiating basic value objects or standard utility data structures is acceptable as they carry state, not business dependency logic (Correct)
C) Only if String is converted to character arrays

9. In C++, how are dependencies typically represented to achieve inversion?

A) Private static methods
B) Smart pointers referencing virtual interface classes (Correct)
C) Global database namespace links

10. What type of injection is recommended for mandatory class dependencies?

A) Setter injection
B) Constructor injection (Correct)
C) Field injection using reflection annotations only

21. Next Lesson Preview

Congratulations! You have completed all five SOLID design principles. In the next module, we will explore UML Diagrams, starting with Class Diagrams, to learn how to visually represent classes, attributes, operations, and relationships in system designs!