IoC Container¶
The Inversion of Control (IoC) container manages dependency injection and automatically wires object graphs from type hints.
What is Dependency Injection?¶
Without DI, classes create dependencies directly:
class UserController:
def __init__(self) -> None:
self._user_use_case = UserUseCase()
self._jwt_service = JWTService()
With DI, dependencies are provided externally:
class UserController:
def __init__(self, user_use_case: UserUseCase, jwt_service: JWTService) -> None:
self._user_use_case = user_use_case
self._jwt_service = jwt_service
The diwire Container¶
This project uses diwire. The container is configured to recursively auto-register missing dependencies.
resolve(UserUseCase) recursively builds dependencies from constructor type hints and applies diwire's default lifetime behavior.
Container Creation¶
src/fastdjango/ioc/container.py creates and configures the container:
from diwire import Container, DependencyRegistrationPolicy, MissingPolicy
def get_container(
*,
configure_django: bool = True,
configure_logging: bool = True,
configure_logfire: bool = True,
instrument_libraries: bool = True,
) -> Container:
container = Container(
missing_policy=MissingPolicy.REGISTER_RECURSIVE,
dependency_registration_policy=DependencyRegistrationPolicy.REGISTER_RECURSIVE,
)
if configure_django:
_configure_django(container)
if configure_logging:
_configure_logging(container)
if configure_logfire:
_configure_logfire(container)
if instrument_libraries:
_instrument_libraries(container)
register_dependencies(container)
return container
Bootstrap Module for FastAPIFactory¶
The HTTP wiring creates the container in a bootstrap module, where get_container is invoked at import time:
# src/fastdjango/entrypoints/fastapi/bootstrap.py
from fastdjango.ioc.container import get_container
container = get_container()
Then the app entrypoint uses ordinary top-level imports (no delayed/lazy import behavior):
from fastdjango.entrypoints.fastapi.bootstrap import container
from fastdjango.entrypoints.fastapi.factories import FastAPIFactory
api_factory = container.resolve(FastAPIFactory)
This keeps startup explicit and centralized in bootstrap.py.
Registration APIs¶
Most services need no manual registration, but when needed use native diwire APIs:
# Register a concrete class for itself
container.add(UserUseCase)
# Register a callable factory class for the value it builds
container.add_factory_class(TasksRegistryFactory, provides=TasksRegistry)
# Register an existing instance/mock
container.add_instance(mock_service, provides=UserUseCase)
Lifetime and Scope¶
Scope and Lifetime are diwire concepts, but this project does not pass
root_scope or default_lifetime to Container().
The container setup in this codebase uses these Container() constructor options:
missing_policy=MissingPolicy.REGISTER_RECURSIVEdependency_registration_policy=DependencyRegistrationPolicy.REGISTER_RECURSIVE
from diwire import Container, DependencyRegistrationPolicy, MissingPolicy
container = Container(
missing_policy=MissingPolicy.REGISTER_RECURSIVE,
dependency_registration_policy=DependencyRegistrationPolicy.REGISTER_RECURSIVE,
)
Scope/Lifetime behavior therefore comes from diwire defaults unless you
explicitly override it when creating Container().
Pydantic Settings Integration¶
diwire resolves BaseSettings subclasses directly, so settings classes can be injected without custom wrappers.
Testing Overrides¶
Each test should get a fresh container. Override dependencies before first resolve of the target dependency graph.