Dependency Injection
Oridecon is built on a powerful, lightweight Dependency Injection (DI) container. Instead of your components creating their own dependencies, they are “injected” at runtime, leading to cleaner code, easier testing, and true modularity.
1. The IoC Container
Section titled “1. The IoC Container”The DI container (Inversion of Control) is the central registry where all application services live. You interact with it primarily through Providers or by resolving services directly from the Application instance.
Registration Scopes
Section titled “Registration Scopes”Oridecon supports three primary registration lifetimes:
| Scope | Method | Description |
|---|---|---|
| Singleton | container.singleton() | Only one instance is created for the entire application lifecycle. |
| Scoped | container.scoped() | A new instance is created per request/operation (common in Web controllers). |
| Transient | container.transient() | A new instance is created every time the dependency is requested. |
Container API
Section titled “Container API”from oridecon import Container
container = Container()
# Registration — key: type to resolve, value: what to returncontainer.singleton(PaymentGateway, StripeGateway(api_key)) # protocol → instancecontainer.singleton(UserService, UserService()) # class → pre-built instancecontainer.scoped(DbSession, SqlAlchemySession) # one per scopecontainer.transient(RequestContext, RequestContext) # new instance each resolve
# Resolutionservice = await container.resolve(PaymentGateway) # returns the StripeGateway instanceoptional = await container.resolve_optional(CacheBackend) # None if not registeredall_impls = await container.resolve_all(BaseHandler) # all subtypes
# Scopingasync with container.scope() as scoped: session = await scoped.resolve(DbSession) # scoped to this block2. Constructor Injection
Section titled “2. Constructor Injection”This is the preferred way to handle dependencies. By simply type-hinting your constructor parameters with a Protocol or Class, Oridecon will automatically resolve and inject the correct instance.
class ProductService: def __init__(self, repo: ProductRepositoryProtocol) -> None: # `repo` is resolved from the container by the constructor type hint self.repo = repo
async def list_products(self) -> list[Product]: return await self.repo.list_all()Type-hint a protocol, not a concrete class. The container injects whatever implementation was bound in register().
3. DI Decorators
Section titled “3. DI Decorators”Oridecon provides decorators to mark classes for automatic discovery and registration:
| Decorator | Scope | Import From |
|---|---|---|
@singleton | One instance for the app | oridecon or oridecon.di |
@injectable | Transient by default | oridecon or oridecon.di |
@scoped | One instance per request/scope | oridecon or oridecon.di |
@transient | New instance each time | oridecon or oridecon.di |
from oridecon import singleton, injectable, scoped
@singletonclass ConfigService: def __init__(self) -> None: self.settings = load_settings()
@injectable # transient by defaultclass UserService: def __init__(self, config: ConfigService) -> None: self.config = config
@scopedclass RequestContext: def __init__(self) -> None: self.request_id = generate_id()How Auto-Registration Works
Section titled “How Auto-Registration Works”@singletonmarksGreetingServicewith__oridecon_injectable__metadataApplication.discover_providers()scans the package and finds the marked class- At boot, the container registers
GreetingServiceas a singleton - When
HelloControlleris instantiated, the container resolvesGreetingServicefrom the constructor type hints
4. Resolving Manually
Section titled “4. Resolving Manually”While constructor injection is preferred, you can also resolve dependencies manually from the container when necessary.
from oridecon.contracts.core.di import BootContainerProtocol
async def boot(self, container: BootContainerProtocol) -> None: repo = await container.resolve(ProductRepositoryProtocol) await repo.connect()boot() takes BootContainerProtocol (resolve + bind()). After freeze, singleton() / transient() / scoped() raise ContainerError (ORI_ERR_DI_001). See Container Protocols.
5. Named Registrations
Section titled “5. Named Registrations”You can register services with names for more granular resolution:
container.singleton( CacheBackend, RedisCacheBackend(), name="redis")
# Resolve by name using Annotatedfrom typing import Annotatedfrom oridecon.di.markers import Named
cache = await container.resolve(Annotated[CacheBackend, Named("redis")])6. Testing with Overrides
Section titled “6. Testing with Overrides”In testing scenarios, you can override service registrations:
container = Container(testing_mode=True)
# Override with fakecontainer.override(UserRepository, FakeUserRepository())Note:
override()is only available in containers created withtesting_mode=True.
Do not pass the container into a service (service locator). Type-hint the protocol on the constructor; the container wires it.
App providers generated by the CLI land in src/<app>/di/ (oridecon gen provider billing). Do not invent src/<app>/providers/ or models/.
Next Steps
Section titled “Next Steps”- Providers — register vs boot, priorities, config injection
- Container Protocols — why the two phases take different types
- Project Structure — where
*_provider.pylives - Testing — fakes and
testing_mode