Dependency boundaries
A dependency-injection container does not make an application modular by itself. Clean IoC provides construction and ownership mechanisms that can support a modular design, but interface quality and behavioral contracts remain application concerns.
Composition root
Object construction, implementation selection, and lifespan policy belong at an application boundary:
builder.register(UserRepository, SqlUserRepository, lifespan="scoped")
builder.register(UserService)
container = builder.build()
UserService remains responsible for application behavior. The composition root is responsible for selecting
SqlUserRepository and assigning its ownership boundary.
Abstractions and implementations
High-level application services can depend on protocols or base classes while infrastructure supplies concrete implementations:
from typing import Protocol
class Notifier(Protocol):
def send(self, message: str) -> None: ...
builder.register(Notifier, EmailNotifier)
Changing the mapping does not require a change to consumers of Notifier. Clean IoC checks that the selected component
can be constructed; it does not verify the behavioral semantics of the implementation.
Narrow interfaces and shared implementations
Several focused service types can refer to one concrete component when they require shared identity:
from clean_ioc.factories import use_component
builder.register(MySender)
builder.register(Sender, factory=use_component(MySender))
builder.register(BatchSender, factory=use_component(MySender))
use_component() creates compiler-visible edges to MySender and preserves per_resolution identity during resolution.
Validation limits
The compiler validates component selection, dependency cycles, generic specialization, decorator and pre-configuration edges, and lifespan ownership. It cannot validate domain behavior, interface cohesion, or substitutability. Those remain properties of the application and its tests.