Skip to content
Packages Examples Agents Blog Get started

Core Concepts

The Application class is the composition root. It manages providers, modules, configuration, and lifecycle.

oridecon run boots <app>.app:app. List the modules this app actually uses:

from oridecon import Application, OrideconConfig
from oridecon.web import WebModule
def create_app(config: OrideconConfig | None = None) -> Application:
application = Application(name="my-app", config=config)
application.add_modules(
[
WebModule.configure(discover=["my_app.controllers", "my_app.modules"]),
]
)
return application
app = create_app()

Key methods on Application:

MethodPurpose
add_module(module)Register a @module class or DynamicModule
add_provider(provider)Register a standalone Provider (rare — prefer modules)
discover_providers(*packages)Scan packages (e.g. my_app.di) for Provider subclasses
start()Boot all providers (register → freeze → boot)
stop()Shutdown in reverse order
Application.boot(...)Context manager — start(), yield, stop()

Application.boot() is for scripts and tests, not the HTTP composition root:

async with Application.boot(
name="test",
modules=[WebModule.stub()],
) as app:
...

Providers register services in the DI container and manage their lifecycle. Generate one with oridecon gen provider billing — the file lands in src/<app>/di/. Every provider follows a two-phase pattern:

from oridecon.di.provider import Provider
from oridecon.contracts.core import ProviderPriority
from oridecon.contracts.core.di import (
ContainerRegistrarProtocol,
BootContainerProtocol,
)
class AppProvider(Provider):
name = "app"
priority = ProviderPriority.DOMAIN
async def register(self, container: ContainerRegistrarProtocol) -> None:
"""Phase 1: Declare bindings. No resolving allowed."""
from my_app.services.user_service import UserService
container.singleton(UserService, UserService)
async def boot(self, container: BootContainerProtocol) -> None:
"""Phase 2: Initialize resources. Resolving is now safe."""
service = await container.resolve(UserService)
await service.warmup_cache()

Providers boot in ascending order. Lower values run first:

PriorityValuePurposeExample
CRITICAL0Absolutely foundationalConfig, diagnostics
INFRASTRUCTURE10Low-level plumbingDatabase, cache, message brokers
SECURITY20Auth infrastructureAuth, encryption
NORMAL30Everyday services (default)Generic services
APPLICATION40Application-level toolsCLI, admin utilities
DOMAIN50Business logicYour domain providers
PRESENTATION80Entry pointsWebModule (registers WebProvider)
COMMS90Outbound communicationEmail, SMS, webhooks
LOW100Optional, boot lastPlugins, analytics

Providers can declare config_key and config_model to automatically receive their typed configuration section from application.yaml:

class CacheProvider(Provider):
name = "cache"
config_key = "cache" # reads the "cache:" section
config_model = CacheConfig # coerces it into CacheConfig
async def register(self, container: ContainerRegistrarProtocol) -> None:
# self.config is now a typed CacheConfig — injected by the orchestrator
cfg = self.config or CacheConfig()
container.singleton(CacheBackend, RedisCacheBackend(cfg))

The ProviderOrchestrator calls OrideconConfig.get_section(config_key, config_model) before register() and assigns the result to provider.config.

HookPhaseWhen Called
register(container)RegistrationContainer open for bindings
boot(container)BootContainer frozen, resolution allowed
shutdown()ShutdownApplication stopping
on_error(error, phase)ErrorWhen boot() or shutdown() raises
health_check(timeout)HealthAggregated by Application.health_check()

Oridecon uses constructor injection — declare dependencies as type hints, the container resolves them automatically:

class OrderService:
def __init__(
self,
repo: OrderRepositoryProtocol, # ← resolved from container
event_bus: EventBusProtocol, # ← resolved from container
) -> None:
self.repo = repo
self.event_bus = event_bus

The container uses a key → value binding pattern. The first argument is the type you resolve by, the second is what you get back:

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.singleton(CacheBackend, factory=create_redis) # lazy factory
container.scoped(DbSession, SqlAlchemySession) # one per scope (class as factory)
container.transient(RequestContext, RequestContext) # new instance each resolve
# Resolution
service = await container.resolve(PaymentGateway) # returns the StripeGateway instance
optional = await container.resolve_optional(EventBus) # None if not registered
all_impls = await container.resolve_all(BaseHandler) # all subtypes
sync_val = container.resolve_sync(UserService) # sync (instantiated singletons only)
# Scoping
async with container.scope() as scoped:
session = await scoped.resolve(DbSession) # scoped to this block
# Lifecycle
container.freeze() # no more registrations allowed
container.override(Service, fake) # testing_mode=True only
await container.dispose() # cleanup all singletons
DecoratorScopeImport From
@singletonOne instance for the apporidecon
@injectableTransient by defaultoridecon
@scopedOne instance per request/scopeoridecon
@transientNew instance each timeoridecon
@injectEnable DI on async functionsoridecon
from oridecon import singleton, injectable, inject
@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
@inject
async def handle_request(user_svc: UserService) -> None:
# user_svc resolved via container.call() or Invoker.invoke()
...

Oridecon uses Result[T, E] for operations that can fail — no exceptions for expected errors.

from oridecon.result import Result, Ok, Err
async def find_user(self, user_id: str) -> Result[User, DomainError]:
user = await self.repo.get(user_id)
if not user:
return Err(UserNotFound(user_id))
return Ok(user)
result = await service.find_user("user-42")
# Check + unwrap
if result.is_ok():
user = result.unwrap()
if result.is_err():
error = result.unwrap_err()
# Safe access
user = result.unwrap_or(default_user)
user = result.unwrap_or_else(lambda e: create_fallback(e))
# Pattern matching
message = result.match(
ok=lambda user: user.name,
err=lambda error: str(error),
)
# Chaining (sync)
name = result.map_sync(lambda u: u.name).unwrap_or("anonymous")
# Chaining (async)
profile = await result.map(fetch_profile).and_then(enrich_profile)
# Filtering
valid = result.filter(lambda u: u.is_active, InactiveUserError())
# Side effects without transformation
result.inspect(lambda u: logger.info("found", user=u.id))
result.inspect_err(lambda e: logger.warning("failed", error=str(e)))
# Nesting
nested: Result[Result[str, E], E] = Ok(Ok("hello"))
flat = nested.flatten() # → Ok("hello")
# Bridge from exceptions
try:
data = json.loads(raw)
except ValueError as e:
return Result.from_exception(e)
from oridecon.result import (
as_result, # decorator: wraps async fn exceptions into Err
as_result_sync, # decorator: wraps sync fn exceptions into Err
collect, # list[Result[T, E]] → Result[list[T], E]
partition, # list[Result[T, E]] → (list[T], list[E])
try_catch, # async: try/catch → Result
try_catch_sync, # sync: try/catch → Result
ResultPipeline, # chainable pipeline builder
)

When a feature needs a boundary, modules add encapsulation over providers. Services inside a module are private by default — only explicitly exported types are visible to importers. The directory tree does not change shape; see Project Structure.

from oridecon.di.module import module
@module(
imports=[AuthModule], # can use AuthServiceProtocol
providers=[BillingProvider],
exports=[BillingServiceProtocol], # only this is visible outside
)
class BillingModule:
"""Billing — depends on auth for user verification."""

For infrastructure that needs runtime configuration, override configure() on the Module base class:

from oridecon.di.module import module, Module, DynamicModule
@module()
class DatabaseModule(Module):
@classmethod
def configure(cls, url: str) -> DynamicModule:
return DynamicModule(
module=cls,
providers=[DatabaseProvider(url=url)],
exports=[DatabaseSession, TransactionManager],
is_global=True, # visible to all modules
)

Usage in your app:

app.add_module(DatabaseModule.configure("postgresql://localhost/mydb"))

The Module base class provides three factory methods:

MethodPurpose
configure(*args, **kwargs)Global configuration — called once at the app root
scope(*providers)Register additional providers in a per-feature scope
stub(config)Return a test-mode module with in-memory/noop backends

A @global_module’s exports are visible to all modules without explicit import:

from oridecon.di.module import global_module, Module
@global_module
class LoggingModule(Module):
providers = [LoggingProvider]
exports = [LoggerProtocol]
Unscoped (app root)Inside a module
Every service is globally visibleServices are private by default
Any class can depend on any otherOnly exported protocols are accessible
No encapsulation boundariesModuleCompiler validates the import/export graph at boot

┌─────────────────────────────────────────┐
│ Application(name, config) │ AppState.CREATED
└─────────────────┬───────────────────────┘
│ app.start()
┌─────────────────────────────────────────┐
│ ModuleCompiler.compile() (if modules) │ Validates import/export graph
└─────────────────┬───────────────────────┘
┌─────────────────────────────────────────┐
│ Provider.register() — all providers │ Bind services (no resolving)
└─────────────────┬───────────────────────┘
┌─────────────────────────────────────────┐
│ Container.freeze() │ No more registrations
└─────────────────┬───────────────────────┘
┌─────────────────────────────────────────┐
│ Provider.boot() — all providers │ Initialize resources (resolving OK)
└─────────────────┬───────────────────────┘
│ AppState.RUNNING
┌─────────────────────────────────────────┐
│ Application running │ Serving traffic
└─────────────────┬───────────────────────┘
│ app.stop()
┌─────────────────────────────────────────┐
│ Provider.shutdown() — reverse order │ Cleanup resources
└─────────────────┬───────────────────────┘
┌─────────────────────────────────────────┐
│ Container.dispose() │ AppState.STOPPED
└─────────────────────────────────────────┘