D — Dependency Inversion Principle
Introduction
Imagine an executive who personally drives to the post office, prints letters on a specific printer, and hand-delivers packages. If the post office changes its address, the printer breaks, or the delivery service changes hours — the executive's entire workflow stops. The executive is tightly coupled to low-level operational details.
A better approach: the executive defines what needs to happen ("send this letter," "deliver this package") and delegates to assistants through well-defined roles. The executive depends on the role ("mail handler"), not the specific person or tool. If the assistant changes, the executive's workflow continues unchanged.
In software, the same problem occurs when high-level business logic directly creates and calls low-level implementation classes. If the database changes from MySQL to PostgreSQL, or the email service changes from SendGrid to SES, the business logic must be rewritten.
The Dependency Inversion Principle (DIP) is the "D" in SOLID. It states:
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.
In simpler terms: your business rules should never import or reference concrete infrastructure classes directly. Instead, define interfaces (abstractions) that describe what you need, and let the infrastructure implement those interfaces.
DIP is the principle that inverts the traditional dependency direction. Instead of business logic depending on infrastructure, infrastructure depends on interfaces defined by business logic. This makes your core code immune to infrastructure changes.

The Problem: Downward Dependencies
In a traditional layered architecture, dependencies flow downward:
- High-level module (business rules) depends on mid-level module (service layer), which depends on low-level module (database driver, email API, file system).
This feels natural — "the business logic needs the database, so it imports the database class." But this creates two severe problems:
Problem 1: High-Level Code Cannot Be Tested Without Low-Level Infrastructure
If OrderService directly creates a MySQLDatabase object, you cannot test OrderService without a running MySQL instance. Unit testing becomes integration testing.
Problem 2: Changing Low-Level Details Breaks High-Level Code
If you switch from MySQL to PostgreSQL, or from SendGrid email to AWS SES, you must modify the OrderService class — even though the business logic (placing an order, calculating totals) has not changed at all.
Let us see this problem in code.
# VIOLATION: High-level OrderService directly depends on
# low-level MySQLDatabase and SendGridEmailer
import mysql.connector # Direct dependency on MySQL driver
class MySQLDatabase:
"""Low-level: specific database technology."""
def __init__(self):
self.connection = mysql.connector.connect(
host="localhost",
user="root",
password="secret",
database="shop"
)
def insert_order(self, order_data: dict) -> int:
cursor = self.connection.cursor()
cursor.execute(
"INSERT INTO orders (customer, total) "
"VALUES (%s, %s)",
(order_data["customer"],
order_data["total"])
)
self.connection.commit()
return cursor.lastrowid
class SendGridEmailer:
"""Low-level: specific email service."""
def send(self, to: str, subject: str,
body: str) -> None:
# Calls SendGrid API directly
print(f"SendGrid: sending '{subject}' to {to}")
class OrderService:
"""High-level: business logic for placing orders.
PROBLEM: directly creates low-level dependencies."""
def __init__(self):
# Hardcoded dependencies — cannot swap, cannot test
self.database = MySQLDatabase()
self.emailer = SendGridEmailer()
def place_order(self, customer: str,
items: list[dict]) -> int:
total = sum(item["price"] * item["qty"]
for item in items)
order_data = {"customer": customer,
"total": total}
# Business logic tied to MySQL specifically
order_id = self.database.insert_order(order_data)
# Business logic tied to SendGrid specifically
self.emailer.send(
to=customer,
subject="Order Confirmed",
body=f"Order #{order_id} total: ${total}"
)
return order_id// VIOLATION: High-level OrderService directly depends on
// low-level MySQLDatabase and SendGridEmailer
class MySQLDatabase {
public int insertOrder(String customer,
double total) {
// Directly uses MySQL JDBC driver
System.out.println("MySQL: INSERT INTO orders...");
return 1; // Simulated order ID
}
}
class SendGridEmailer {
public void send(String to, String subject,
String body) {
System.out.println("SendGrid: sending '"
+ subject + "' to " + to);
}
}
class OrderService {
// Hardcoded low-level dependencies
private final MySQLDatabase database =
new MySQLDatabase();
private final SendGridEmailer emailer =
new SendGridEmailer();
public int placeOrder(String customer,
List<Map<String, Object>> items) {
double total = items.stream()
.mapToDouble(i ->
(double) i.get("price") *
(int) i.get("qty"))
.sum();
int orderId = database.insertOrder(
customer, total);
emailer.send(customer, "Order Confirmed",
"Order #" + orderId + " total: $" + total);
return orderId;
}
}The dependency arrows point downward:
OrderService → MySQLDatabase (concrete class)
OrderService → SendGridEmailer (concrete class)
If you want to switch to PostgreSQL, you must modify OrderService. If you want to test without a real database, you cannot — OrderService creates MySQLDatabase internally. The high-level business rules are held hostage by low-level infrastructure choices.
Applying DIP — Inverting the Dependencies
The fix is to introduce abstractions (interfaces) that the high-level module defines and the low-level modules implement. The dependency arrows are inverted:
- Before:
OrderService→MySQLDatabase - After:
OrderService→OrderRepository(interface) ←MySQLOrderRepository
The high-level module depends on the abstraction. The low-level module also depends on the abstraction (by implementing it). Neither depends on the other directly.
# FIX: Depend on abstractions, not concrete implementations
from abc import ABC, abstractmethod
# --- Abstractions (owned by the high-level module) ---
class OrderRepository(ABC):
"""Abstraction: how orders are persisted.
Defined by the business layer — not by any database."""
@abstractmethod
def save_order(self, order_data: dict) -> int:
pass
class NotificationService(ABC):
"""Abstraction: how notifications are sent.
Defined by the business layer — not by any email
provider."""
@abstractmethod
def notify(self, recipient: str, subject: str,
message: str) -> None:
pass
# --- High-level module: depends only on abstractions ---
class OrderService:
"""Business logic — pure domain rules.
No knowledge of MySQL, PostgreSQL, SendGrid, or SES."""
def __init__(self, repository: OrderRepository,
notifier: NotificationService):
self._repository = repository
self._notifier = notifier
def place_order(self, customer: str,
items: list[dict]) -> int:
total = sum(item["price"] * item["qty"]
for item in items)
order_data = {"customer": customer,
"total": total}
order_id = self._repository.save_order(order_data)
self._notifier.notify(
recipient=customer,
subject="Order Confirmed",
message=f"Order #{order_id} total: ${total}"
)
return order_id
# --- Low-level modules: implement the abstractions ---
class MySQLOrderRepository(OrderRepository):
def save_order(self, order_data: dict) -> int:
print(f"MySQL: saved order for "
f"{order_data['customer']}")
return 1
class PostgreSQLOrderRepository(OrderRepository):
def save_order(self, order_data: dict) -> int:
print(f"PostgreSQL: saved order for "
f"{order_data['customer']}")
return 1
class SendGridNotifier(NotificationService):
def notify(self, recipient: str, subject: str,
message: str) -> None:
print(f"SendGrid: '{subject}' to {recipient}")
class SESNotifier(NotificationService):
def notify(self, recipient: str, subject: str,
message: str) -> None:
print(f"AWS SES: '{subject}' to {recipient}")
# --- Composition: wire dependencies from the outside ---
# Production setup
service = OrderService(
repository=MySQLOrderRepository(),
notifier=SendGridNotifier()
)
service.place_order("alice@mail.com",
[{"price": 29.99, "qty": 2}])
# Swapped infrastructure — ZERO changes to OrderService
service_v2 = OrderService(
repository=PostgreSQLOrderRepository(),
notifier=SESNotifier()
)
service_v2.place_order("bob@mail.com",
[{"price": 49.99, "qty": 1}])// FIX: Depend on abstractions, not concrete implementations
// --- Abstractions (owned by the business layer) ---
interface OrderRepository {
int saveOrder(String customer, double total);
}
interface NotificationService {
void notify(String recipient, String subject,
String message);
}
// --- High-level module: depends only on abstractions ---
class OrderService {
private final OrderRepository repository;
private final NotificationService notifier;
public OrderService(OrderRepository repository,
NotificationService notifier) {
this.repository = repository;
this.notifier = notifier;
}
public int placeOrder(String customer,
List<Map<String, Object>> items) {
double total = items.stream()
.mapToDouble(i ->
(double) i.get("price") *
(int) i.get("qty"))
.sum();
int orderId = repository.saveOrder(
customer, total);
notifier.notify(customer, "Order Confirmed",
"Order #" + orderId + " total: $" + total);
return orderId;
}
}
// --- Low-level modules: implement the abstractions ---
class MySQLOrderRepository implements OrderRepository {
@Override
public int saveOrder(String customer, double total) {
System.out.println("MySQL: saved order for "
+ customer);
return 1;
}
}
class PostgreSQLOrderRepository
implements OrderRepository {
@Override
public int saveOrder(String customer, double total) {
System.out.println("PostgreSQL: saved order for "
+ customer);
return 1;
}
}
class SendGridNotifier implements NotificationService {
@Override
public void notify(String recipient, String subject,
String message) {
System.out.println("SendGrid: '" + subject
+ "' to " + recipient);
}
}
// --- Composition root ---
public class Main {
public static void main(String[] args) {
// Production
OrderService service = new OrderService(
new MySQLOrderRepository(),
new SendGridNotifier()
);
service.placeOrder("alice@mail.com",
List.of(Map.of("price", 29.99, "qty", 2)));
// Swapped — zero changes to OrderService
OrderService serviceV2 = new OrderService(
new PostgreSQLOrderRepository(),
new SendGridNotifier()
);
serviceV2.placeOrder("bob@mail.com",
List.of(Map.of("price", 49.99, "qty", 1)));
}
}The dependency arrows are now inverted:
OrderServicedepends onOrderRepository(abstraction) — not on MySQL.MySQLOrderRepositorydepends onOrderRepository(abstraction) — by implementing it.- Neither high-level nor low-level knows about the other. Both depend on the abstraction.
Key changes:
-
Abstractions defined by the business layer:
OrderRepositoryandNotificationServiceare defined based on what the business needs, not what the database supports. -
Constructor injection:
OrderServicereceives its dependencies through the constructor, not by creating them internally. -
Swappable implementations: Switching from MySQL to PostgreSQL requires zero changes to
OrderService— just pass a different implementation at construction time. -
Testable: You can pass a mock/fake
OrderRepositoryfor unit testing without any real database.
DIP vs. Dependency Injection — Clarifying the Confusion
DIP and Dependency Injection are related but distinct concepts. They are frequently confused because they share the word "dependency" and often appear together.
Dependency Inversion Principle (DIP)
A design principle — it tells you how to structure your dependencies. High-level modules should depend on abstractions, not on low-level implementations. DIP is about the direction of dependency arrows in your architecture.
Dependency Injection (DI)
A technique — it tells you how to provide dependencies to a class. Instead of a class creating its own dependencies internally (self.db = MySQLDatabase()), dependencies are "injected" from the outside (passed through the constructor, setter, or method parameter).
How They Relate
DIP answers: "What should I depend on?" → Abstractions.
DI answers: "How do I provide those abstractions?" → Inject them from outside.
You can use DI without DIP — injecting a concrete MySQLDatabase through the constructor gives you swappability but not true inversion. And you can conceptually follow DIP without formal DI frameworks. But in practice, they work best together.
We will dive deep into Dependency Injection patterns — constructor injection, setter injection, interface injection, and DI containers — in S13: Dependency Injection. DIP gives you the why. DI gives you the how.
Visualization

How to Detect DIP Violations
Signal 1: Importing Concrete Infrastructure in Business Logic Files
If your order_service.py imports mysql.connector, boto3, sendgrid, or any specific library — the high-level module directly depends on low-level details.
Signal 2: new / Direct Construction of Dependencies
If a class creates its own dependencies inside __init__ (e.g., self.db = MySQLDatabase()), it is tightly coupled. The class controls which implementation is used, making it impossible to substitute.
Signal 3: Changes in Infrastructure Force Business Logic Changes
If switching from MySQL to PostgreSQL requires modifying 15 business logic files — you have a DIP violation. The business logic should not know or care which database flavor is used.
Signal 4: You Cannot Unit Test Without Real Infrastructure
If testing OrderService requires a running database, a real email server, or a live API key — the dependencies are not inverted. With proper DIP, you pass mock implementations for testing.
Common Mistakes
Mistake 1: Creating an Abstraction That Mirrors the Concrete Class Exactly
The trap: You create OrderRepository with methods that match MySQL's exact capabilities — executeQuery(sql), commit(), rollback(). The abstraction is just a thin wrapper around MySQL.
The fix: Define the abstraction from the business perspective — save_order(order_data), find_order(order_id). The abstraction should describe what the business needs, not how the database works. This is what makes switching databases possible.
Mistake 2: Abstracting Everything, Including Simple Utilities
The trap: "DIP says depend on abstractions, so I will create interfaces for MathUtils, StringFormatter, and DateParser."
The fix: DIP primarily applies at architectural boundaries — where business logic meets infrastructure (databases, APIs, file systems, external services). Simple utility classes that have no infrastructure dependency and will never be swapped do not need abstractions.
Mistake 3: Confusing DIP with "Just Add an Interface"
The trap: You create IOrderService as an interface for OrderService, but OrderService still internally creates MySQLDatabase. Adding an interface on top of the high-level module does not invert anything.
The fix: The interfaces belong at the boundary between high-level and low-level — OrderRepository (between business logic and database), NotificationService (between business logic and email provider). The high-level module defines the interface; the low-level module implements it.
DIP and the Rest of SOLID
DIP is the final SOLID principle, and it ties the others together:
-
DIP + SRP: Each abstraction should represent a single responsibility.
OrderRepositoryhandles persistence.NotificationServicehandles notifications. This is SRP applied at the interface level. -
DIP + OCP: With dependencies inverted through abstractions, adding a new database backend means creating a new
OrderRepositoryimplementation — without modifyingOrderService. This is OCP in action. -
DIP + LSP: Every implementation of
OrderRepositorymust be substitutable — it must fulfill the contract. LSP guarantees that swapping implementations (the promise of DIP) actually works. -
DIP + ISP: The abstractions should be focused (ISP). If
OrderRepositoryhas methods for orders, inventory, AND reporting, it is a fat interface. ISP says split it so implementations only handle what they support.
Together, the SOLID principles form a cohesive design philosophy: SRP structures your classes, OCP makes them extensible, LSP ensures substitutability, ISP keeps interfaces focused, and DIP decouples your architecture. In the next tutorial, we will apply all five principles together to a single realistic example.
Key Takeaways
- The Dependency Inversion Principle says high-level modules should depend on abstractions, not on low-level implementations — and low-level modules should implement those abstractions
- The dependency arrows are inverted: instead of business logic pointing down to infrastructure, infrastructure points up to interfaces defined by business logic
- Define abstractions from the business perspective (
save_order) not the infrastructure perspective (executeQuery) - DIP enables swapping infrastructure (MySQL → PostgreSQL) and unit testing with mocks without changing business logic
- DIP is a principle (what to depend on). Dependency Injection is a technique (how to provide dependencies). They are complementary — we will cover DI patterns in depth in S13: Dependency Injection
- Apply DIP at architectural boundaries — where business logic meets databases, APIs, file systems, and external services. Do not over-abstract simple utilities
- Next up: SOLID in Action — we will take one realistic class and refactor it step-by-step, applying all five SOLID principles