Loading episodes…
0:00 0:00

Mastering the Dependency Inversion Principle (DIP): A Visual Guide with Python

00:00
BACK TO HOME

Mastering the Dependency Inversion Principle (DIP): A Visual Guide with Python

10xTeam November 06, 2025 11 min read

The Dependency Inversion Principle (DIP) is the final pillar of the SOLID design principles. It provides a powerful strategy for creating decoupled, flexible, and robust software architectures. At its core, DIP revolutionizes how different parts of your application interact.

The principle is formally defined by two key rules:

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

[!NOTE] High-Level vs. Low-Level Modules

  • High-Level Modules: These contain the core business logic and policies of your application (e.g., an OrderProcessor that orchestrates placing an order).
  • Low-Level Modules: These handle detailed, secondary operations like writing to a database, sending an email, or logging to a file (e.g., a PostgresDatabase class).

Let’s visualize the core idea. Imagine an old camera that only works with one specific type of lens. The camera (high-level module) is completely dependent on that single lens (low-level module). If you want to use a zoom or macro lens, you’re out of luck. This is a tightly coupled system.

Now, consider a modern DSLR. It has a standardized mounting ring—an abstraction. Any lens that conforms to this standard mount will work, whether it’s a wide-angle, telephoto, or macro lens. The camera no longer depends on a specific lens; it depends on the abstract mount. This is Dependency Inversion in action.

Here’s a summary of what we’ll cover:

mindmap
  root((Dependency Inversion Principle))
    The Problem
      Tightly Coupled Code
      Rigid & Fragile
      Hard to Test
    The Solution
      "Abstractions (e.g., ABCs)"
      Dependency Injection
      Decoupled Code
    Benefits
      Flexibility
      Testability
      Maintainability

The Problem: A Tightly Coupled Order Processing System

Let’s imagine our business needs a system to process customer orders. The requirements are:

  1. Calculate the total price of an order.
  2. Save the order details to a MySQL database.
  3. Send a confirmation email to the customer.

A straightforward, initial implementation might look like this:

dip-project-before/
├── main.py
└── services/
    ├── database.py
    ├── notifier.py
    └── order_processor.py

Here’s the code, where the high-level OrderProcessor directly creates and uses low-level MySqlDatabase and EmailNotifier instances.

services/database.py

# Low-level module
class MySqlDatabase:
    def save_order(self, order_details: dict):
        print(f"Saving order {order_details['id']} to MySQL database.")
        # Logic to connect and save to MySQL...

services/notifier.py

# Low-level module
class EmailNotifier:
    def send_notification(self, customer_email: str, message: str):
        print(f"Sending email to {customer_email}: {message}")
        # Logic to send email via an SMTP server...

services/order_processor.py

# High-level module
from .database import MySqlDatabase
from .notifier import EmailNotifier

class OrderProcessor:
    def __init__(self):
        # Direct, tight coupling to concrete low-level modules
        self.db = MySqlDatabase()
        self.notifier = EmailNotifier()

    def process(self, order: dict):
        print("Processing order...")
        # 1. Calculate total (dummy logic)
        order['total'] = 100.00
        
        # 2. Save to database
        self.db.save_order(order)
        
        # 3. Send notification
        message = f"Your order {order['id']} has been processed."
        self.notifier.send_notification(order['customer_email'], message)

This code works, but it’s fundamentally flawed. It violates DIP because the high-level OrderProcessor directly depends on the low-level MySqlDatabase and EmailNotifier.

graph TD
    subgraph "Tightly Coupled Architecture"
        OrderProcessor -- "depends directly on" --> MySqlDatabase
        OrderProcessor -- "depends directly on" --> EmailNotifier
    end

    style OrderProcessor fill:#f9f,stroke:#333,stroke-width:2px
    style MySqlDatabase fill:#ccf,stroke:#333,stroke-width:2px
    style EmailNotifier fill:#ccf,stroke:#333,stroke-width:2px

What happens when the requirements change?

  • “We’re migrating from MySQL to PostgreSQL!”
  • “We want to send SMS notifications instead of emails for certain orders.”

To accommodate these changes, you would have to modify the OrderProcessor class itself. You’d need to add if/else logic or completely swap out the hardcoded dependencies. This violates the Open/Closed Principle and makes the system brittle and difficult to maintain.

The Solution: Inverting Dependencies with Abstractions

To fix this, we introduce an abstraction layer. In Python, the ideal tool for this is the abc (Abstract Base Class) module. We’ll define contracts (IDatabase and INotifier) that our low-level modules must adhere to.

The new project structure will include these abstractions:

dip-project-after/
├── main.py
└── services/
    ├── abstractions.py  # <-- New file for our contracts
    ├── database.py
    ├── notifier.py
    └── order_processor.py

1. Create the Abstractions

services/abstractions.py

from abc import ABC, abstractmethod

class IDatabase(ABC):
    """Abstract contract for all database repositories."""
    @abstractmethod
    def save_order(self, order_details: dict):
        pass

class INotifier(ABC):
    """Abstract contract for all notification services."""
    @abstractmethod
    def send_notification(self, customer_contact: str, message: str):
        pass

2. Implement the Details

Now, our concrete classes will implement these abstract interfaces.

services/database.py

from .abstractions import IDatabase

# Detail
class MySqlDatabase(IDatabase):
    def save_order(self, order_details: dict):
        print(f"Saving order {order_details['id']} to MySQL database.")
        # ...

# Another detail
class PostgresDatabase(IDatabase):
    def save_order(self, order_details: dict):
        print(f"Saving order {order_details['id']} to PostgreSQL database.")
        # ...

services/notifier.py

from .abstractions import INotifier

# Detail
class EmailNotifier(INotifier):
    def send_notification(self, customer_contact: str, message: str):
        print(f"Sending email to {customer_contact}: {message}")
        # ...

# Another detail
class SmsNotifier(INotifier):
    def send_notification(self, customer_contact: str, message: str):
        print(f"Sending SMS to {customer_contact}: {message}")
        # ...

3. Refactor the High-Level Module

Finally, we refactor OrderProcessor to depend on the abstractions, not the concrete details. We’ll use Dependency Injection through the constructor to supply the required dependencies.

--- a/services/order_processor_before.py
+++ b/services/order_processor_after.py
@@ -1,12 +1,11 @@
 # High-level module
-from .database import MySqlDatabase
-from .notifier import EmailNotifier
+from .abstractions import IDatabase, INotifier
 
 class OrderProcessor:
-    def __init__(self):
-        # Direct, tight coupling to concrete low-level modules
-        self.db = MySqlDatabase()
-        self.notifier = EmailNotifier()
+    def __init__(self, database: IDatabase, notifier: INotifier):
+        # Depends on abstractions, not concretions!
+        self.db = database
+        self.notifier = notifier
 
     def process(self, order: dict):
         print("Processing order...")
@@ -16,5 +15,5 @@
         self.db.save_order(order)
         
         # 3. Send notification
         message = f"Your order {order['id']} has been processed."
-        self.notifier.send_notification(order['customer_email'], message)
+        self.notifier.send_notification(order['customer_contact'], message)

Our architecture now looks like this:

classDiagram
    direction LR

    class OrderProcessor {
        -IDatabase db
        -INotifier notifier
        +process(order)
    }

    class IDatabase {
        <<interface>>
        +save_order(order)
    }

    class INotifier {
        <<interface>>
        +send_notification(contact, message)
    }

    OrderProcessor ..> IDatabase : depends on
    OrderProcessor ..> INotifier : depends on

    IDatabase <|-- MySqlDatabase
    IDatabase <|-- PostgresDatabase

    INotifier <|-- EmailNotifier
    INotifier <|-- SmsNotifier

    class MySqlDatabase { +save_order(order) }
    class PostgresDatabase { +save_order(order) }
    class EmailNotifier { +send_notification(contact, message) }
    class SmsNotifier { +send_notification(contact, message) }

The OrderProcessor is now completely decoupled from the specific database or notification technology. It only knows about the IDatabase and INotifier contracts.

Putting It All Together: The Composition Root

The final piece of the puzzle is the “Composition Root”—the entry point of our application where we “compose” our objects and inject the concrete dependencies.

main.py

from services.order_processor import OrderProcessor
from services.database import MySqlDatabase, PostgresDatabase
from services.notifier import EmailNotifier, SmsNotifier

def main():
    order_1 = {'id': 'ORD-101', 'customer_contact': 'customer@example.com'}
    order_2 = {'id': 'ORD-102', 'customer_contact': '+1234567890'}

    print("--- Scenario 1: Using MySQL and Email ---")
    # Compose the OrderProcessor with MySQL and Email services
    mysql_processor = OrderProcessor(
        database=MySqlDatabase(),
        notifier=EmailNotifier()
    )
    mysql_processor.process(order_1)

    print("\n--- Scenario 2: Using PostgreSQL and SMS ---")
    # Compose the OrderProcessor with PostgreSQL and SMS services
    postgres_processor = OrderProcessor(
        database=PostgresDatabase(),
        notifier=SmsNotifier()
    )
    postgres_processor.process(order_2)

if __name__ == "__main__":
    main()

Output:

--- Scenario 1: Using MySQL and Email ---
Processing order...
Saving order ORD-101 to MySQL database.
Sending email to customer@example.com: Your order ORD-101 has been processed.

--- Scenario 2: Using PostgreSQL and SMS ---
Processing order...
Saving order ORD-102 to PostgreSQL database.
Sending SMS to +1234567890: Your order ORD-102 has been processed.

As you can see, we can now change the application’s behavior without modifying the high-level OrderProcessor at all. We simply inject different implementations at runtime.

Deep Dive: Dependency Inversion vs. Dependency Injection

These two concepts are related but distinct:
  • Dependency Inversion Principle (DIP) is a design principle. It's the "what" and "why"—the idea that high-level modules should depend on abstractions.
  • Dependency Injection (DI) is a design pattern. It's the "how"—one of the techniques used to implement DIP. DI is the act of passing a dependency (like a database service) to a client object from an external source, rather than having the client create it internally. Constructor injection, as used in our example, is the most common form of DI.

The Benefits of Dependency Inversion

  1. Flexibility & Scalability: Swapping implementations is trivial. Adding a new database type (e.g., MongoDbDatabase) only requires creating a new class that implements IDatabase. No other code needs to change.
  2. Enhanced Testability: This is one of the biggest wins. When testing OrderProcessor, we don’t need a real database or email server. We can inject mock objects that simulate their behavior.

    # A simple test using a mock object
    class MockDatabase(IDatabase):
        def save_order(self, order_details: dict):
            print("Mock save called. No real database needed!")
            self.saved = True
    
    def test_order_processor_saves_to_db():
        # Arrange
        mock_db = MockDatabase()
        mock_notifier = EmailNotifier() # Or a mock notifier
        processor = OrderProcessor(database=mock_db, notifier=mock_notifier)
        order = {'id': 'TEST-001', 'customer_contact': 'test@test.com'}
    
        # Act
        processor.process(order)
    
        # Assert
        assert mock_db.saved is True
    
  3. Improved Maintainability: Code is easier to reason about because components are isolated. Changes in a low-level module (like optimizing a database query) won’t break a high-level module as long as the contract is respected.

[!WARNING] Beware of Over-Engineering While powerful, DIP is not a silver bullet. Applying abstractions everywhere can lead to unnecessary complexity for simple applications. Use it where flexibility is a known or highly anticipated requirement. Don’t abstract things that are truly stable and unlikely to change.

By embracing the Dependency Inversion Principle, you shift your focus from concrete details to abstract contracts, building systems that are not just functional, but also resilient, adaptable, and ready for future evolution.


Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?