Skip to content
Packages Examples Agents Blog Get started

Service Providers

Service Providers are the fundamental building blocks of Oridecon applications. They encapsulate the logic for registering services, configuring infrastructure, and managing the initial boot sequence of your application.

Generate one into app-root di/ — never providers/ or models/:

Terminal window
oridecon gen provider billing # src/<app>/di/billing_provider.py

Module-local providers stay at src/<app>/modules/<slug>/provider.py after oridecon new module.

Every provider implements two primary methods that are called sequentially by the Application during startup.

Used to register bindings in the DI container. At this stage, you should only define how services are created, not start them.

from oridecon import Provider
from oridecon.contracts.core import ProviderPriority
from oridecon.contracts.core.di import ContainerRegistrarProtocol
class DatabaseProvider(Provider):
name = "database"
priority = ProviderPriority.INFRASTRUCTURE
async def register(self, container: ContainerRegistrarProtocol) -> None:
# Bind the protocol to our specific implementation
container.singleton(DatabaseProtocol, MySqlConnection)
container.singleton(DatabaseService, DatabaseService())

Invoked after all providers have finished their registration phase. This is the safe place to perform I/O, connect to databases, or resolve dependencies that rely on other providers.

from oridecon.contracts.core.di import BootContainerProtocol
async def boot(self, container: BootContainerProtocol) -> None:
db = await container.resolve(DatabaseProtocol)
await db.connect()
container.bind(DatabaseService, DatabaseService(db))

Note: boot() receives BootContainerProtocol. The container is frozen during boot, so singleton()/transient()/scoped() raise ContainerError (ORI_ERR_DI_001). To replace an already-registered singleton (e.g. swapping in a configured DatabaseService), use container.bind(service_type, instance).


Oridecon uses a priority system to ensure that lower-level infrastructure (like Logging or Database) is ready before high-level application code (like Web Controllers) starts.

PriorityValueUse Case
CRITICAL0Absolutely foundational services (configuration, diagnostics)
INFRASTRUCTURE10Low-level plumbing (database, cache, message brokers)
SECURITY20Authentication/authorization infrastructure
NORMAL30Everyday domain services (default)
APPLICATION40Application-level tools (CLI, admin utilities)
DOMAIN50Business-logic providers
PRESENTATION80Web/API layers and entry points
COMMS90Outbound communication (email, SMS, webhooks)
LOW100Optional providers that can boot last
from oridecon import Provider
from oridecon.contracts.core import ProviderPriority
class MyInfraProvider(Provider):
name = "my-infra"
priority = ProviderPriority.INFRASTRUCTURE

Providers boot in ascending priority order (lower values boot first). This ensures:

  • CRITICAL (0) services boot before everything else
  • INFRASTRUCTURE (10) services boot before DOMAIN (50) services
  • PRESENTATION (80) services boot last

from oridecon import Provider
from oridecon.contracts.core import ProviderPriority
class BillingProvider(Provider):
name = "billing" # Unique identifier
priority = ProviderPriority.APPLICATION # Boot order
dependencies = ("database", "cache") # Wait for these providers first
optional_dependencies = ("metrics",) # These may not exist
boot_timeout = 30.0 # Max seconds for boot()
required = True # App fails if this fails to boot
provider = BillingProvider(
name="billing",
priority=ProviderPriority.DOMAIN,
dependencies=("database",),
optional_dependencies=("cache",),
boot_timeout=60.0,
required=False,
)

HookPhaseWhen Called
register(container)RegistrationContainer open for bindings only
boot(container)BootContainer frozen, resolution allowed
shutdown()ShutdownApplication stopping, reverse order
on_error(error, phase)ErrorWhen boot() or shutdown() raises
health_check(timeout)HealthAggregated by Application.health_check()
from oridecon.logging import get_logger
log = get_logger(__name__)
async def on_error(self, error: Exception, phase: str) -> None:
"""Called when boot() or shutdown() raises an exception."""
log.error("provider.failed", name=self.name, phase=phase, error=str(error))

Use Application.discover_providers() to scan packages for Provider subclasses:

from oridecon import Application
app = Application(name="my-app")
# Explicit registration (rare — the scaffold uses modules)
app.add_provider(MyDbProvider())
# Auto-discovery — scan di/, not a providers/ directory
app.discover_providers("my_app.di", "my_app.infrastructure")

The discover_providers() method scans each package recursively for:

  • Provider subclasses with a no-argument constructor
  • @injectable / @singleton decorated classes

The composition root still prefers create_app() + modules. Discovery is extra surface, not a second layout.


Providers can automatically receive typed configuration from application.yaml:

from dataclasses import dataclass
from oridecon import Provider
from oridecon.contracts.core.di import ContainerRegistrarProtocol
@dataclass
class BillingConfig:
stripe_key: str = ""
currency: str = "usd"
class BillingProvider(Provider):
name = "billing"
config_key = "billing" # Reads "billing:" section from YAML
config_model = BillingConfig # Coerces into BillingConfig
async def register(self, container: ContainerRegistrarProtocol) -> None:
cfg = self.config or BillingConfig()
container.singleton(StripeClient, StripeClient(cfg.stripe_key))

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


from oridecon import Provider
from oridecon.contracts.core import ProviderPriority, HealthCheckResult, HealthStatus
from oridecon.contracts.core.di import ContainerRegistrarProtocol, BootContainerProtocol
class CacheProvider(Provider):
name = "cache"
priority = ProviderPriority.INFRASTRUCTURE
dependencies = ("config",)
async def register(self, container: ContainerRegistrarProtocol) -> None:
from my_app.infrastructure.cache import CacheBackend
from my_app.infrastructure.cache.redis import RedisCache
# Register the protocol, not the concrete implementation
container.singleton(CacheBackend, RedisCache)
async def boot(self, container: BootContainerProtocol) -> None:
cache = await container.resolve(CacheBackend)
await cache.connect()
async def shutdown(self) -> None:
# Cleanup happens here
pass
async def health_check(self, timeout: float = 5.0) -> HealthCheckResult:
return HealthCheckResult(component=self.name, status=HealthStatus.HEALTHY)

Do not put business logic on the provider class. Bind in register(), connect in boot(), return Result from services.