Skip to main content

Dependency Injection & Inversion of Control

Externalize dependencies for maximum flexibility, testability, and loose coupling

TL;DR

Dependency Injection inverts control: instead of objects creating their dependencies, dependencies are provided (injected) from outside. This decouples code, enables easy testing with mocks, and makes configurations explicit and swappable.

Learning Objectives

  • You will be able to distinguish constructor, setter, and interface injection approaches.
  • You will be able to refactor tightly coupled code into injectable dependencies.
  • You will be able to implement a simple DI container or use an existing one effectively.
  • You will be able to test code with injected dependencies using mocks and stubs.

Motivating Scenario

Your UserService directly instantiates DatabaseConnection and EmailService. Testing it requires real databases and email. Mocking is impossible. Adding a cache layer means modifying UserService. Instead, pass these dependencies to the constructor. Tests inject mocks. Production wires real implementations. Swapping implementations requires changing configuration, not code.

Core Concepts

Dependency Injection provides objects with their dependencies rather than having them create dependencies themselves. This follows the Inversion of Control (IoC) principle—control over object creation moves from the object to an external container or framework.

Three injection styles:

  • Constructor Injection: dependencies provided in the constructor (preferred)
  • Setter Injection: dependencies set via property setters (flexible but less safe)
  • Interface Injection: object implements an interface to receive dependencies
Dependency Injection vs. tight coupling

Practical Example

from abc import ABC, abstractmethod

# Abstractions (contracts)
class Database(ABC):
@abstractmethod
def query(self, sql):
pass

class EmailSender(ABC):
@abstractmethod
def send(self, to: str, message: str):
pass

# Concrete implementations
class PostgresDatabase(Database):
def query(self, sql):
return f"PostgreSQL result for: {sql}"

class MockDatabase(Database):
def query(self, sql):
return f"Mock result for: {sql}"

class SMTPEmailSender(EmailSender):
def send(self, to: str, message: str):
return f"Email sent to {to}: {message}"

class MockEmailSender(EmailSender):
def __init__(self):
self.sent_emails = []

def send(self, to: str, message: str):
self.sent_emails.append((to, message))
return f"Mock: email recorded"

# Service with constructor injection
class UserService:
def __init__(self, db: Database, email: EmailSender):
self.db = db
self.email = email

def create_user(self, name, email_addr):
# Use injected dependencies
result = self.db.query(f"INSERT INTO users VALUES ('{name}')")
self.email.send(email_addr, f"Welcome {name}")
return result

# Simple DI Container (in real life, use frameworks like Dependency Injector)
class DIContainer:
def __init__(self):
self.services = {}

def register(self, name, factory):
self.services[name] = factory

def get(self, name):
return self.services[name]()

# Setup
container = DIContainer()
container.register('db', lambda: PostgresDatabase())
container.register('email', lambda: SMTPEmailSender())

# Production
service = UserService(container.get('db'), container.get('email'))
service.create_user("Alice", "alice@example.com")

# Testing
mock_db = MockDatabase()
mock_email = MockEmailSender()
test_service = UserService(mock_db, mock_email)
test_service.create_user("TestUser", "test@example.com")
print(f"Emails sent in test: {mock_email.sent_emails}")

When to Use / When Not to Use

Use Dependency Injection when:
  1. Code needs to be easily testable with mocks/stubs
  2. You want to swap implementations without changing code
  3. Objects have dependencies on other objects
  4. Dependencies vary across environments (dev, test, prod)
  5. You want to make dependencies explicit and visible
  6. Tight coupling is preventing code reuse
Consider alternatives when:
  1. Objects are truly stateless utilities (static methods are fine)
  2. DI framework overhead doesn't justify complexity
  3. Dependencies are simple or constant (hardcoding is acceptable)
  4. You're building a tiny script (over-engineering)
  5. Language doesn't support it well (rare today)

Patterns and Pitfalls

Patterns and Pitfalls

Constructor injection enforces required dependencies; setter injection allows optional ones.
Service Locator hides dependencies, making them invisible to callers.
Centralize dependency configuration in one place (usually at application startup).

Design Review Checklist

  • Dependencies are provided via constructor, not created internally
  • Classes depend on abstractions (interfaces), not concrete implementations
  • Testable: dependencies can be mocked or stubbed easily
  • No Service Locator anti-pattern (no hidden dependencies)
  • Composition root exists: one place where dependencies are wired
  • Circular dependencies are identified and resolved (or redesigned)
  • Optional dependencies are clearly marked (e.g., with defaults or Optional types)
  • Object graphs are manageable (not overly complex)
  • DI framework/container is optional but not required for small apps

Self-Check

  1. Identify: Find classes that directly instantiate their dependencies. Refactor them to accept injected parameters.
  2. Test: Write unit tests for a class with injected dependencies using mocks.
  3. Wire: Create a composition root that wires up all services for a simple application.
info

One Takeaway: Dependency Injection makes code testable, flexible, and loosely coupled. Instead of objects creating their dependencies, pass them in. This enables painless testing with mocks and easy swapping of implementations.

Next Steps

  • Learn Service Locator pattern (and why it's usually worse than DI).
  • Study Inversion of Control containers for larger applications (Spring, .NET DI, Python Dependency Injector).
  • Explore Composition Root pattern for organizing service setup.

References

  • Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern
  • Google's Guice (Java DI framework)
  • Robert C. Martin: Clean Code (Chapter 11: System Construction)
  • Dependency Injection Principles, Practices, and Patterns (book)