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
Challenge: Fix the Weather App
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 DIP With DIP new keyword everywhereInject dependencies Tied to specific implementations Tied to abstractions Hard to test Easy to mock Hard to change Easy to swap Tight coupling Loose 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!