Skip to content
Packages Examples Agents Blog Get started

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.

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.

Oridecon supports three primary registration lifetimes:

ScopeMethodDescription
Singletoncontainer.singleton()Only one instance is created for the entire application lifecycle.
Scopedcontainer.scoped()A new instance is created per request/operation (common in Web controllers).
Transientcontainer.transient()A new instance is created every time the dependency is requested.
from oridecon import Container
container = Container()
# Registration — key: type to resolve, value: what to return
container.singleton(PaymentGateway, StripeGateway(api_key)) # protocol → instance
container.singleton(UserService, UserService()) # class → pre-built instance
container.scoped(DbSession, SqlAlchemySession) # one per scope
container.transient(RequestContext, RequestContext) # new instance each resolve
# Resolution
service = await container.resolve(PaymentGateway) # returns the StripeGateway instance
optional = await container.resolve_optional(CacheBackend) # None if not registered
all_impls = await container.resolve_all(BaseHandler) # all subtypes
# Scoping
async with container.scope() as scoped:
session = await scoped.resolve(DbSession) # scoped to this block

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().


Oridecon provides decorators to mark classes for automatic discovery and registration:

DecoratorScopeImport From
@singletonOne instance for the apporidecon or oridecon.di
@injectableTransient by defaultoridecon or oridecon.di
@scopedOne instance per request/scopeoridecon or oridecon.di
@transientNew instance each timeoridecon or oridecon.di
from oridecon import singleton, injectable, scoped
@singleton
class ConfigService:
def __init__(self) -> None:
self.settings = load_settings()
@injectable # transient by default
class UserService:
def __init__(self, config: ConfigService) -> None:
self.config = config
@scoped
class RequestContext:
def __init__(self) -> None:
self.request_id = generate_id()
  1. @singleton marks GreetingService with __oridecon_injectable__ metadata
  2. Application.discover_providers() scans the package and finds the marked class
  3. At boot, the container registers GreetingService as a singleton
  4. When HelloController is instantiated, the container resolves GreetingService from the constructor type hints

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.


You can register services with names for more granular resolution:

container.singleton(
CacheBackend,
RedisCacheBackend(),
name="redis"
)
# Resolve by name using Annotated
from typing import Annotated
from oridecon.di.markers import Named
cache = await container.resolve(Annotated[CacheBackend, Named("redis")])

In testing scenarios, you can override service registrations:

container = Container(testing_mode=True)
# Override with fake
container.override(UserRepository, FakeUserRepository())

Note: override() is only available in containers created with testing_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/.