Skip to content

Dependency Inversion Principle

Depend on abstractions, not on concretions.

The Dependency Inversion Principle (DIP) states that:

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

This principle helps create flexible, maintainable systems by inverting the traditional dependency flow.

Diagram Diagram

The Dependency Inversion Principle ensures that:

  • High-level modules (business logic) don’t depend on low-level modules (implementation details)
  • Both depend on abstractions (interfaces)
  • Details depend on abstractions, not the other way around
  • Flexibility - Easy to swap implementations
  • Testability - Easy to mock dependencies

In simple terms: Depend on what something can do (interface), not how it does it (implementation)!

Consider a simple light switch system. Let’s see how direct dependencies create problems.

Diagram
bad_light.py
class LightBulb:
"""Low-level module - concrete implementation"""
def turn_on(self):
print("Light bulb turned on")
def turn_off(self):
print("Light bulb turned off")
class LightSwitch:
"""❌ High-level module - depends on concrete LightBulb"""
def __init__(self):
self.bulb = LightBulb() # ❌ Direct dependency on concrete class!
def toggle(self):
if self.bulb.is_on: # Assuming we track state
self.bulb.turn_off()
else:
self.bulb.turn_on()
# Problem: What if we want to use LED instead of LightBulb?
class LED:
"""New type of light"""
def turn_on(self):
print("LED turned on")
def turn_off(self):
print("LED turned off")
# ❌ Can't use LED with LightSwitch without modifying LightSwitch!
# LightSwitch is tightly coupled to LightBulb
Diagram
light.py
from abc import ABC, abstractmethod
class Switchable(ABC):
"""Abstraction - defines what a switchable device can do"""
@abstractmethod
def turn_on(self):
"""Turn the device on"""
pass
@abstractmethod
def turn_off(self):
"""Turn the device off"""
pass
class LightBulb(Switchable):
"""Low-level module - concrete implementation"""
def __init__(self):
self.is_on = False
def turn_on(self):
self.is_on = True
print("Light bulb turned on")
def turn_off(self):
self.is_on = False
print("Light bulb turned off")
class LED(Switchable):
"""Low-level module - another concrete implementation"""
def __init__(self):
self.is_on = False
def turn_on(self):
self.is_on = True
print("LED turned on")
def turn_off(self):
self.is_on = False
print("LED turned off")
class LightSwitch:
"""✓ High-level module - depends on abstraction"""
def __init__(self, device: Switchable): # ✓ Depends on abstraction!
self.device = device
self.is_on = False
def toggle(self):
if self.is_on:
self.device.turn_off()
self.is_on = False
else:
self.device.turn_on()
self.is_on = True
# Usage - Flexible and testable!
bulb = LightBulb()
switch1 = LightSwitch(bulb) # ✓ Works with LightBulb
switch1.toggle() # Turns on bulb
led = LED()
switch2 = LightSwitch(led) # ✓ Works with LED - no code changes!
switch2.toggle() # Turns on LED
# Easy to add new devices without modifying LightSwitch!
class Fan(Switchable):
def turn_on(self):
print("Fan turned on")
def turn_off(self):
print("Fan turned off")
fan = Fan()
switch3 = LightSwitch(fan) # ✓ Works with Fan too!
switch3.toggle()

Why this follows DIP:

  • LightSwitch depends on Switchable abstraction, not concrete classes
  • Can easily swap implementations without modifying LightSwitch
  • Easy to test - can create mock Switchable implementations
  • New devices can be added without changing existing code

Consider an e-commerce order processing system that needs to send notifications and process payments.

Diagram
bad_order.py
class EmailService:
"""Low-level module - concrete email implementation"""
def send_email(self, to: str, subject: str, body: str):
print(f"Sending email to {to}: {subject}")
# Actual email sending logic...
class StripePaymentProcessor:
"""Low-level module - concrete payment implementation"""
def charge(self, amount: float, card_token: str):
print(f"Charging ${amount} via Stripe")
# Actual Stripe API call...
class OrderProcessor:
"""❌ High-level module - depends on concrete implementations"""
def __init__(self):
self.email_service = EmailService() # ❌ Direct dependency!
self.payment_processor = StripePaymentProcessor() # ❌ Direct dependency!
def process_order(self, order: dict):
"""Process an order"""
# Process payment
self.payment_processor.charge(
order['amount'],
order['card_token']
)
# Send confirmation email
self.email_service.send_email(
to=order['email'],
subject="Order Confirmation",
body=f"Your order for ${order['amount']} has been processed"
)

Why this violates DIP:

  • OrderProcessor directly depends on concrete EmailService and StripePaymentProcessor
  • Can’t swap implementations without modifying OrderProcessor
  • Hard to test - can’t inject mock dependencies
  • Tight coupling reduces flexibility
Diagram
order.py
from abc import ABC, abstractmethod
# Abstractions - define what services can do
class NotificationService(ABC):
"""Abstraction for notification services"""
@abstractmethod
def send_notification(self, to: str, message: str):
"""Send a notification"""
pass
class PaymentProcessor(ABC):
"""Abstraction for payment processors"""
@abstractmethod
def process_payment(self, amount: float, details: dict) -> bool:
"""Process a payment and return success status"""
pass
# Low-level modules - concrete implementations
class EmailService(NotificationService):
"""Low-level module - email implementation"""
def send_notification(self, to: str, message: str):
print(f"Sending email to {to}: {message}")
# Actual email sending logic...
class SMSService(NotificationService):
"""Low-level module - SMS implementation"""
def send_notification(self, to: str, message: str):
print(f"Sending SMS to {to}: {message}")
# Actual SMS sending logic...
class StripePaymentProcessor(PaymentProcessor):
"""Low-level module - Stripe implementation"""
def process_payment(self, amount: float, details: dict) -> bool:
print(f"Processing ${amount} via Stripe")
# Actual Stripe API call...
return True
class PayPalPaymentProcessor(PaymentProcessor):
"""Low-level module - PayPal implementation"""
def process_payment(self, amount: float, details: dict) -> bool:
print(f"Processing ${amount} via PayPal")
# Actual PayPal API call...
return True
# High-level module - depends on abstractions
class OrderProcessor:
"""✓ High-level module - depends on abstractions"""
def __init__(
self,
notification_service: NotificationService, # ✓ Abstraction!
payment_processor: PaymentProcessor # ✓ Abstraction!
):
self.notification_service = notification_service
self.payment_processor = payment_processor
def process_order(self, order: dict):
"""Process an order"""
# Process payment
success = self.payment_processor.process_payment(
order['amount'],
order['payment_details']
)
if success:
# Send confirmation notification
self.notification_service.send_notification(
to=order['email'],
message=f"Your order for ${order['amount']} has been processed"
)
# Usage - Flexible and testable!
# Can easily swap implementations
email_service = EmailService()
stripe_processor = StripePaymentProcessor()
order_processor1 = OrderProcessor(email_service, stripe_processor)
# Switch to SMS and PayPal - no code changes needed!
sms_service = SMSService()
paypal_processor = PayPalPaymentProcessor()
order_processor2 = OrderProcessor(sms_service, paypal_processor)
# Easy to test with mocks
class MockNotificationService(NotificationService):
def send_notification(self, to: str, message: str):
print(f"[MOCK] Would send to {to}: {message}")
class MockPaymentProcessor(PaymentProcessor):
def process_payment(self, amount: float, details: dict) -> bool:
print(f"[MOCK] Would process ${amount}")
return True
mock_notification = MockNotificationService()
mock_payment = MockPaymentProcessor()
test_processor = OrderProcessor(mock_notification, mock_payment)
# Easy to test without real services!

Why this follows DIP:

  • OrderProcessor depends on NotificationService and PaymentProcessor abstractions
  • Can easily swap implementations (Email → SMS, Stripe → PayPal)
  • Easy to test - can inject mock implementations
  • New services can be added without modifying OrderProcessor
  • Loose coupling makes the system flexible and maintainable

Dependency Injection (DI) is a technique that helps implement DIP by:

  • Injecting dependencies instead of creating them internally
  • Making dependencies explicit through constructor parameters
  • Enabling easy swapping of implementations
bad_di.py
class UserService:
"""❌ Creates dependencies internally"""
def __init__(self):
self.repository = DatabaseRepository() # ❌ Hard-coded dependency!
self.logger = FileLogger() # ❌ Hard-coded dependency!
def get_user(self, user_id: int):
self.logger.log(f"Getting user {user_id}")
return self.repository.find(user_id)
good_di.py
class UserService:
"""✓ Receives dependencies via constructor"""
def __init__(self, repository: UserRepository, logger: Logger):
self.repository = repository # ✓ Injected dependency!
self.logger = logger # ✓ Injected dependency!
def get_user(self, user_id: int):
self.logger.log(f"Getting user {user_id}")
return self.repository.find(user_id)
# Usage - Easy to swap implementations
database_repo = DatabaseRepository()
file_logger = FileLogger()
service1 = UserService(database_repo, file_logger)
# Switch to in-memory repo and console logger
memory_repo = InMemoryRepository()
console_logger = ConsoleLogger()
service2 = UserService(memory_repo, console_logger) # ✓ No code changes!

Remember: The Dependency Inversion Principle ensures that your system is flexible, testable, and maintainable by depending on abstractions rather than concrete implementations! 🎯