Dependency Injection Explained Without the Buzzwords

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.