Step 2: IoC Registration¶
In this step, you will register the TodoService in the IoC (Inversion of Control) container so it can be automatically injected into controllers.
Files Overview¶
| Action | File Path |
|---|---|
| Modify | src/ioc/registries/core.py |
Understanding the Container¶
The template uses punq, a lightweight dependency injection container for Python. The container is configured in src/ioc/container.py and organized into registries by layer:
src/ioc/
├── container.py # Main container factory
└── registries/
├── core.py # Domain services and settings
├── delivery.py # Controllers and factories
└── infrastructure.py # Cross-cutting concerns (JWT, auth)
Registration Flow¶
When the application starts, it creates a container and registers all dependencies:
# src/ioc/container.py (simplified)
def get_container() -> Container:
container = Container()
register_core(container) # Services, settings
register_infrastructure(container) # JWT, auth
register_delivery(container) # Controllers
return container
Step 2.1: Register TodoService¶
Open src/ioc/registries/core.py and add the TodoService registration.
First, add the import at the top of the file:
from punq import Container, Scope
from configs.core import ApplicationSettings
from core.health.services import HealthService
from core.todo.services import TodoService # Add this import
from core.user.services.user import UserService
Then add the registration in the _register_services function:
def _register_services(container: Container) -> None:
container.register(HealthService, scope=Scope.singleton)
container.register(UserService, scope=Scope.singleton)
container.register(TodoService, scope=Scope.singleton) # Add this line
Complete Updated File¶
Here is the complete src/ioc/registries/core.py after the changes:
from punq import Container, Scope
from configs.core import ApplicationSettings
from core.health.services import HealthService
from core.todo.services import TodoService
from core.user.services.user import UserService
def register_core(container: Container) -> None:
_register_settings(container)
_register_services(container)
def _register_settings(container: Container) -> None:
container.register(
ApplicationSettings,
factory=lambda: ApplicationSettings(),
scope=Scope.singleton,
)
def _register_services(container: Container) -> None:
container.register(HealthService, scope=Scope.singleton)
container.register(UserService, scope=Scope.singleton)
container.register(TodoService, scope=Scope.singleton)
Understanding Registration Options¶
The container.register() method supports several patterns:
Type-Based Registration (Recommended)¶
The container automatically:
- Inspects
TodoService.__init__for dependencies - Resolves each dependency from the container
- Creates an instance with the resolved dependencies
Since TodoService has no constructor dependencies, punq creates it with no arguments.
Factory-Based Registration¶
For services that require special initialization:
container.register(
ApplicationSettings,
factory=lambda: ApplicationSettings(),
scope=Scope.singleton,
)
Use factories when:
- The service loads configuration from environment variables
- You need custom initialization logic
- The service requires arguments not managed by the container
Instance/Factory Registration for Protocols¶
For mapping protocols to concrete implementations:
container.register(
ApplicationSettingsProtocol,
factory=lambda: container.resolve(ApplicationSettings),
scope=Scope.singleton,
)
Use this pattern when:
- You need to register a specific implementation for a protocol
- The instance is pre-created (e.g., a configuration object)
Scopes¶
The container supports two scopes:
| Scope | Behavior |
|---|---|
Scope.singleton |
One instance shared across the application |
Scope.transient |
New instance created for each resolution |
When to Use Each Scope
- Singleton: Services that maintain state or are expensive to create
- Transient: Services that should be fresh for each request
For most services in this template, Scope.singleton is appropriate because:
- Services are stateless (they don't store request-specific data)
- Creating new instances adds unnecessary overhead
- Django ORM handles database connections separately
Step 2.2: Verify Registration¶
You can verify the registration works by testing in the Django shell:
>>> from ioc.container import get_container
>>> from core.todo.services import TodoService
>>>
>>> container = get_container()
>>> service = container.resolve(TodoService)
>>> print(service)
<core.todo.services.TodoService object at 0x...>
>>>
>>> # Verify singleton behavior
>>> service2 = container.resolve(TodoService)
>>> print(service is service2)
True
Registration Complete
If you see True, the service is properly registered as a singleton.
How Dependency Resolution Works¶
When a controller depends on TodoService, the container automatically provides it:
class TodoController(Controller):
def __init__(self, todo_service: TodoService) -> None:
self._todo_service = todo_service
The resolution chain works like this:
- Container sees
TodoControllerneedsTodoService - Container looks up
TodoServicein its registrations - Container creates (or retrieves singleton)
TodoService - Container creates
TodoControllerwith the resolved service
This automatic wiring eliminates manual dependency management and makes testing easier.
What's Next¶
You have registered TodoService in the IoC container:
- [x] Added import for
TodoService - [x] Registered as a singleton
- [x] Verified resolution works
In the next step, you will create the HTTP API controller that uses this service.
Continue to Step 3: HTTP API & Admin
See Also
- IoC Container - Deep dive into dependency injection with punq