Dependency Injection sounds like enterprise Java ceremony, but the core idea is simple: a class shouldn’t create its own dependencies — they should be handed to it, so they can be swapped or mocked freely.
# WITHOUT dependency injection: hard-coded, untestable dependency
class OrderService:
def __init__(self):
self.db = PostgresConnection() # created internally - impossible to swap for a test
# WITH dependency injection: the dependency is passed in from outside
class OrderService:
def __init__(self, db_connection):
self.db = db_connection # could be Postgres, a mock, an in-memory fake
# In production
service = OrderService(PostgresConnection())
# In a test - no real database needed at all
service = OrderService(FakeInMemoryDatabase())
Why This Matters Beyond Testing
Beyond testability, DI decouples what a class does from how its dependencies are implemented — swapping a payment provider, a logging backend, or a caching layer becomes a configuration change rather than a rewrite of the classes that use them. Frameworks like Spring (Java) or Laravel’s service container automate the “wiring” step, but the underlying principle works in any language, framework or not.