Backend / Architecture & design / 03_dependency_injection.md

Dependency Injection

Updated 4 interview angles 3 min read source
On this page7
  1. What is dependency injection (DI)?
  2. What are the benefits of dependency injection?
  3. How do you implement dependency injection in Python?
  4. How does FastAPI use dependency injection?
  5. What is the difference between DI and a service locator?
  6. Where the wiring goes
  7. Interview angle

Dependency Injection

What is dependency injection (DI)?

Dependency injection is a design pattern where a component receives its dependencies from the outside instead of creating or looking them up itself. The caller (or a container/framework) provides the concrete implementations—e.g. a database connection, a logger, or an API client—so the component stays decoupled from those details and is easier to test and change.

What are the benefits of dependency injection?

  • Loose coupling: Components depend on abstractions or contracts, not on concrete classes.
  • Testability: In tests you inject mocks or stubs (e.g. fake DB, in-memory repo) instead of real implementations.
  • Flexibility: You can swap implementations (e.g. different DB, different LLM provider) without changing the component.
  • Single responsibility: The component focuses on its logic; something else is responsible for wiring dependencies.
  • Explicit dependencies: What a component needs is visible in its constructor or parameters.

How do you implement dependency injection in Python?

  • Constructor injection: Pass dependencies in __init__; store them as attributes. The caller (or a factory) creates the dependencies and the component.
python
class UserService:
    def __init__(self, repo: UserRepository, logger: Logger):
        self.repo = repo
        self.logger = logger

    def get_user(self, id: str):
        return self.repo.find_by_id(id)
  • Function/setter injection: Pass dependencies as arguments to a function or set them via setters. Common in frameworks (e.g. FastAPI Depends() injects into route handlers).
  • Container / framework: Use a DI container (e.g. dependency-injector, or FastAPI’s Depends) to register and resolve dependencies; the framework injects them when creating the component.

How does FastAPI use dependency injection?

FastAPI’s Depends() is a form of DI: you declare dependencies (functions or callables) and FastAPI calls them and injects the return value into the route (or into other dependencies). Used for: DB sessions, auth, config, shared services. Dependencies can be nested (a dependency can depend on another). The framework creates the dependency graph and injects at request time.

What is the difference between DI and a service locator?

  • Dependency injection: The component receives its dependencies (passed in or injected by a framework). It doesn’t know how they were created.
  • Service locator: The component asks a central registry (“give me the user repo”) when it needs something. The component still depends on the locator and on knowing what to ask for.

DI is generally preferred: dependencies are explicit and easier to test; you don’t hide them behind a global lookup.

Where the wiring goes

If every class receives its dependencies, something has to construct them. That place is the composition root — one module, run once at startup, and the only part of the codebase that knows every concrete class:

python
# main.py — the only file that imports both sides.
def build(settings: Settings) -> UserService:
    engine = create_async_engine(settings.database_url)
    return UserService(
        repo=SqlUserRepository(engine),
        mailer=SesMailer(settings.aws_region),
    )

Everything below build takes what it needs as a parameter and imports no concrete implementation. Which is what makes the test version one line:

python
service = UserService(repo=InMemoryUsers(), mailer=NullMailer())

No patching, no container, no import-order surprises. If a test needs monkeypatch to substitute a collaborator, that collaborator is being constructed rather than injected — that is the signal.

Interview angle 4

  • “What is dependency injection, in one sentence?” - a component receives its collaborators from outside rather than constructing them, so the caller decides which implementation it gets.
  • “Why does it matter beyond testing?” - it makes the dependency graph explicit and swappable. Testing is the most visible benefit, but the structural one is that a component’s constructor documents everything it needs, with no hidden globals.
  • “DI versus the dependency inversion principle?” - DI is the mechanism (pass it in); DIP is the design rule (depend on abstractions, and let the high-level module own the abstraction). You can do DI without DIP by injecting concrete classes.
  • “Do you need a container?” - not for a flat graph; constructor arguments wired in a composition root are enough. A container earns its place with deep graphs, differing lifetimes, or when the same graph is needed outside HTTP - Celery tasks and consumers. See DI containers and the dependency-injector library.