Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

🔌 The DIP Rule

“High-level modules should not depend on low-level modules. Both should depend on abstractions.”
Think about how you charge your phone:
  • You plug into a wall socket (abstraction) 🔌
  • You don’t wire directly to the power plant! ⚡
The socket is an interface - any charger that fits will work!
Simple Rule: Instead of creating dependencies directly, depend on interfaces/abstractions and get the real thing injected from outside!
The home appliance analogy: Your refrigerator does not generate its own electricity. It depends on a standard power interface (the wall outlet). The power company can switch from coal to solar to nuclear — your fridge does not care because it depends on the abstraction (standardized outlet), not the implementation (specific power plant). DIP works the same way: your high-level business logic should depend on abstract interfaces, and the concrete implementations (which database, which email provider, which payment gateway) are “plugged in” from the outside.

🎯 What Does “Inversion” Mean?

Traditional dependency (BAD):
HighLevel → creates → LowLevel
Inverted dependency (GOOD):
HighLevel → depends on → Abstraction ← implements ← LowLevel
The control is “inverted” - high-level code doesn’t create its dependencies, it receives them!

🚨 The Problem: Tight Coupling

❌ BAD: Creating Dependencies Inside

class MySQLDatabase:
    def connect(self):
        print("Connecting to MySQL...")
    
    def query(self, sql):
        print(f"MySQL executing: {sql}")
        return [{"id": 1, "name": "Alice"}]

class UserRepository:
    def __init__(self):
        # 🚨 Directly creates MySQL - tightly coupled!
        self.database = MySQLDatabase()
    
    def find_user(self, user_id):
        self.database.connect()
        return self.database.query(f"SELECT * FROM users WHERE id = {user_id}")

# Problems:
# 1. Can't switch to PostgreSQL without changing UserRepository
# 2. Can't test without a real MySQL database
# 3. Can't use a different database for different environments

# Want to switch to PostgreSQL? Must modify UserRepository!
# Want to test? Must have MySQL running!

✅ GOOD: Depend on Abstraction

from abc import ABC, abstractmethod

# 1. Define the abstraction (interface)
class Database(ABC):
    @abstractmethod
    def connect(self):
        pass
    
    @abstractmethod
    def query(self, sql):
        pass

# 2. Implement concrete versions
class MySQLDatabase(Database):
    def connect(self):
        print("🐬 Connecting to MySQL...")
    
    def query(self, sql):
        print(f"🐬 MySQL: {sql}")
        return [{"id": 1, "name": "Alice"}]

class PostgreSQLDatabase(Database):
    def connect(self):
        print("🐘 Connecting to PostgreSQL...")
    
    def query(self, sql):
        print(f"🐘 PostgreSQL: {sql}")
        return [{"id": 1, "name": "Alice"}]

class MockDatabase(Database):
    """For testing!"""
    def connect(self):
        print("🧪 Mock database connected")
    
    def query(self, sql):
        print(f"🧪 Mock query: {sql}")
        return [{"id": 99, "name": "Test User"}]

# 3. Depend on abstraction, receive via injection
# PRINCIPLE: UserRepository does not know or care whether it is
# talking to MySQL, PostgreSQL, or a mock. It only knows the
# Database interface. This is what "inversion" means -- the
# high-level module defines the interface it needs, and the
# low-level module conforms to it.
class UserRepository:
    def __init__(self, database: Database):  # Accepts ANY Database
        self.database = database
    
    def find_user(self, user_id):
        self.database.connect()
        return self.database.query(f"SELECT * FROM users WHERE id = {user_id}")

# 4. Usage - inject whatever you need!
print("=== Production with MySQL ===")
mysql_repo = UserRepository(MySQLDatabase())
mysql_repo.find_user(1)

print("\n=== Production with PostgreSQL ===")
postgres_repo = UserRepository(PostgreSQLDatabase())
postgres_repo.find_user(1)

print("\n=== Testing with Mock ===")
test_repo = UserRepository(MockDatabase())
test_repo.find_user(1)

# 🎉 Same UserRepository code, different databases!

📧 Real Example: Notification System

❌ BAD: Directly Creating Email Client

import smtplib

class NotificationService:
    def __init__(self):
        # 🚨 Directly creating SMTP - tight coupling!
        self.server = smtplib.SMTP('smtp.gmail.com', 587)
        self.server.login('user@gmail.com', 'password')
    
    def send(self, to, message):
        self.server.send_message(message, to_addrs=[to])

# Problems:
# - Can't switch to SendGrid, Mailgun, or AWS SES easily
# - Can't test without sending real emails!
# - Gmail credentials hardcoded

✅ GOOD: Inject Email Provider

from abc import ABC, abstractmethod

class EmailProvider(ABC):
    @abstractmethod
    def send(self, to: str, subject: str, body: str):
        pass

class GmailProvider(EmailProvider):
    def __init__(self, username, password):
        self.username = username
        self.password = password
    
    def send(self, to, subject, body):
        print(f"📧 Gmail: Sending to {to}")
        print(f"   Subject: {subject}")
        # Actual SMTP logic here

class SendGridProvider(EmailProvider):
    def __init__(self, api_key):
        self.api_key = api_key
    
    def send(self, to, subject, body):
        print(f"📧 SendGrid: Sending to {to}")
        print(f"   Subject: {subject}")
        # SendGrid API logic here

class AWSEmailProvider(EmailProvider):
    def __init__(self, access_key, secret_key, region):
        self.region = region
    
    def send(self, to, subject, body):
        print(f"📧 AWS SES ({self.region}): Sending to {to}")
        print(f"   Subject: {subject}")
        # AWS SES logic here

class MockEmailProvider(EmailProvider):
    """For testing - doesn't send real emails!"""
    def __init__(self):
        self.sent_emails = []
    
    def send(self, to, subject, body):
        print(f"🧪 Mock: Would send to {to}")
        self.sent_emails.append({"to": to, "subject": subject, "body": body})

class NotificationService:
    def __init__(self, email_provider: EmailProvider):  # 🎯 Inject provider
        self.email = email_provider
    
    def notify_user(self, user_email, message):
        self.email.send(user_email, "Notification", message)

# Usage - swap providers easily!
print("=== Using Gmail ===")
gmail_notifier = NotificationService(GmailProvider("user", "pass"))
gmail_notifier.notify_user("customer@example.com", "Your order shipped!")

print("\n=== Using SendGrid ===")
sendgrid_notifier = NotificationService(SendGridProvider("api-key-123"))
sendgrid_notifier.notify_user("customer@example.com", "Your order shipped!")

print("\n=== Testing ===")
mock = MockEmailProvider()
test_notifier = NotificationService(mock)
test_notifier.notify_user("test@test.com", "Test message")
print(f"Captured {len(mock.sent_emails)} emails!")  # Can verify emails!

💳 Real Example: Payment Processing

from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class PaymentResult:
    success: bool
    transaction_id: str
    message: str

class PaymentGateway(ABC):
    @abstractmethod
    def process(self, amount: float, card_token: str) -> PaymentResult:
        pass
    
    @abstractmethod
    def refund(self, transaction_id: str) -> PaymentResult:
        pass

class StripeGateway(PaymentGateway):
    def __init__(self, api_key):
        self.api_key = api_key
    
    def process(self, amount, card_token):
        print(f"💳 Stripe: Processing ${amount}")
        return PaymentResult(True, "stripe_tx_123", "Payment successful")
    
    def refund(self, transaction_id):
        print(f"💳 Stripe: Refunding {transaction_id}")
        return PaymentResult(True, transaction_id, "Refund successful")

class PayPalGateway(PaymentGateway):
    def __init__(self, client_id, client_secret):
        self.client_id = client_id
    
    def process(self, amount, card_token):
        print(f"🅿️ PayPal: Processing ${amount}")
        return PaymentResult(True, "paypal_tx_456", "Payment successful")
    
    def refund(self, transaction_id):
        print(f"🅿️ PayPal: Refunding {transaction_id}")
        return PaymentResult(True, transaction_id, "Refund successful")

class SquareGateway(PaymentGateway):
    def __init__(self, access_token):
        self.access_token = access_token
    
    def process(self, amount, card_token):
        print(f"⬜ Square: Processing ${amount}")
        return PaymentResult(True, "square_tx_789", "Payment successful")
    
    def refund(self, transaction_id):
        print(f"⬜ Square: Refunding {transaction_id}")
        return PaymentResult(True, transaction_id, "Refund successful")

class MockPaymentGateway(PaymentGateway):
    """For testing!"""
    def __init__(self, should_succeed=True):
        self.should_succeed = should_succeed
        self.processed = []
    
    def process(self, amount, card_token):
        self.processed.append(amount)
        if self.should_succeed:
            return PaymentResult(True, "mock_tx_000", "Mock success")
        return PaymentResult(False, "", "Mock failure")
    
    def refund(self, transaction_id):
        return PaymentResult(True, transaction_id, "Mock refund")

# Order service depends on abstraction
class OrderService:
    def __init__(self, payment_gateway: PaymentGateway):  # 🎯 Injected!
        self.payment = payment_gateway
    
    def checkout(self, order_total, card_token):
        print(f"🛒 Processing order for ${order_total}")
        result = self.payment.process(order_total, card_token)
        
        if result.success:
            print(f"✅ Order confirmed! Transaction: {result.transaction_id}")
        else:
            print(f"❌ Order failed: {result.message}")
        
        return result

# 🎉 Easy to switch payment providers!

# Production - use Stripe
order_service = OrderService(StripeGateway("sk_live_xxx"))
order_service.checkout(99.99, "tok_visa")

# Different environment - use PayPal
print()
paypal_service = OrderService(PayPalGateway("client_id", "secret"))
paypal_service.checkout(49.99, "paypal_tok")

# Testing - use mock
print("\n=== Testing ===")
mock = MockPaymentGateway(should_succeed=True)
test_service = OrderService(mock)
test_service.checkout(100.00, "test_token")
print(f"Total processed in tests: ${sum(mock.processed)}")

🏗️ Dependency Injection Patterns

1️⃣ Constructor Injection (Most Common)

class UserService:
    def __init__(self, user_repo: UserRepository, email_service: EmailService):
        self.users = user_repo
        self.email = email_service

2️⃣ Setter Injection

class UserService:
    def __init__(self):
        self._user_repo = None
    
    def set_repository(self, repo: UserRepository):
        self._user_repo = repo

3️⃣ Method Injection

class UserService:
    def create_user(self, data, email_service: EmailService):
        # Email service passed only when needed
        email_service.send_welcome(data['email'])

🏭 Simple DI Container

class Container:
    """Simple dependency injection container"""
    
    def __init__(self):
        self._services = {}
    
    def register(self, interface, implementation):
        self._services[interface] = implementation
    
    def resolve(self, interface):
        if interface not in self._services:
            raise KeyError(f"Service {interface} not registered")
        return self._services[interface]

# Setup container
container = Container()

# Register implementations
if ENVIRONMENT == "production":
    container.register("database", MySQLDatabase())
    container.register("email", SendGridProvider("api_key"))
    container.register("payment", StripeGateway("stripe_key"))
else:
    container.register("database", MockDatabase())
    container.register("email", MockEmailProvider())
    container.register("payment", MockPaymentGateway())

# Resolve dependencies
db = container.resolve("database")
email = container.resolve("email")

# Create services with resolved dependencies
user_repo = UserRepository(db)
notification = NotificationService(email)

📊 DIP Benefits Visualization

WITHOUT DIP (Tight Coupling):
┌─────────────────────────────────┐
│         OrderService            │
│                                 │
│  ┌─────────────────────────┐    │
│  │   StripeGateway         │◄───┼── Hardcoded inside!
│  │   (concrete class)      │    │
│  └─────────────────────────┘    │
│                                 │
│  - Can't change gateway         │
│  - Can't test without Stripe    │
│  - Coupled to implementation    │
└─────────────────────────────────┘

WITH DIP (Loose Coupling):
┌─────────────────────────────────┐
│         OrderService            │
│                                 │
│  ┌─────────────────────────┐    │
│  │   PaymentGateway        │◄───┼── Interface (abstraction)
│  │   (abstract interface)  │    │
│  └─────────────────────────┘    │
└─────────────────────────────────┘

           │ implements
    ┌──────┼──────┬─────────────┐
    │      │      │             │
┌───┴───┐ ┌┴────┐ ┌┴─────────┐ ┌┴────────┐
│Stripe │ │PayPal│ │  Square │ │MockPay │
└───────┘ └─────┘ └─────────┘ └────────┘

- Easy to swap implementations
- Easy to test with mocks
- Decoupled from details

🧪 Practice Exercise

This weather app violates DIP. Fix it!
import requests

class WeatherApp:
    def __init__(self):
        # 🚨 Directly coupled to specific API!
        self.api_key = "hardcoded_key"
        self.api_url = "https://api.openweathermap.org/data/2.5/weather"
    
    def get_weather(self, city):
        # 🚨 Can't test without calling real API!
        response = requests.get(
            self.api_url,
            params={"q": city, "appid": self.api_key}
        )
        data = response.json()
        return {
            "city": city,
            "temp": data["main"]["temp"],
            "description": data["weather"][0]["description"]
        }
    
    def display_weather(self, city):
        weather = self.get_weather(city)
        print(f"Weather in {weather['city']}: {weather['temp']}°K")
        print(f"Description: {weather['description']}")

# Problems:
# - Can't switch to different weather API (WeatherStack, AccuWeather)
# - Can't test without internet/real API calls
# - API key hardcoded

📝 Key Takeaways

Without DIPWith DIP
new keyword everywhereInject dependencies
Tied to specific implementationsTied to abstractions
Hard to testEasy to mock
Hard to changeEasy to swap
Tight couplingLoose coupling

🎉 SOLID Complete!

Congratulations! You’ve learned all five SOLID principles:

S - Single Responsibility

One class, one job

O - Open/Closed

Add features without changing code

L - Liskov Substitution

Children replace parents

I - Interface Segregation

Many small interfaces

D - Dependency Inversion

Depend on abstractions

Why DIP Matters in Production

DIP is what makes your system testable, deployable, and adaptable. Consider a payment processing service at a startup. In development, you inject a MockPaymentGateway so tests run in milliseconds without hitting real APIs. In staging, you inject a StripeSandboxGateway that talks to Stripe’s test environment. In production, you inject StripeProductionGateway. The OrderService code is identical across all three environments — only the injected dependency changes. This is also how feature flags work: you can inject a NewPricingEngine for beta users and LegacyPricingEngine for everyone else, controlled by configuration rather than code changes. A senior engineer would say: “DIP is the principle that makes all the other principles practically useful. SRP gives you small classes, OCP makes them extensible, LSP keeps substitution safe, ISP keeps interfaces focused — but DIP is the wiring that connects everything together without creating tight coupling. Constructor injection is the single most important technique for writing testable code.”

Interview Insight

DIP is the “testability” principle in interviews. When an interviewer asks “how would you test this?” and your design has hardcoded dependencies (self.db = MySQLDatabase()), you are stuck — you need a running MySQL instance for every unit test. But if you designed with DIP (self.db = database via constructor injection), the answer is: “I inject a MockDatabase in tests, the real database in production.” This is the single most common design improvement interviewers look for. Beyond testability, DIP shows up when interviewers ask about environment parity: “how does this work in staging vs production?” If your answer is “we inject different implementations of the same interface,” that is DIP in action. Key vocabulary to use: “dependency injection,” “interface-based design,” “loose coupling,” and “constructor injection.”

🏃 Next: Design Patterns

Now that you understand SOLID, let’s learn the classic design patterns that solve common problems!

Continue to Design Patterns →

Learn the proven solutions to recurring design problems!