Skip to content
Packages Examples Agents Blog Get started

Modules

In Oridecon, a Module is a high-level organizational unit that groups related providers, services, and configuration. It serves as an encapsulation boundary, defining a strict “public API” for other parts of the application.

Use the @module decorator to define a module. This tells Oridecon how to treat the package during auto-discovery and DI resolution.

from oridecon.di.module import module
@module(
providers=[ChatProvider],
exports=[ChatServiceProtocol, MessageStoreProtocol],
)
class ChatModule:
"""A module providing AI chat functionality."""
@module()
class AuthModule:
name: str | None = None # Module name (auto-derived if None)
providers: list[type] = [] # Provider classes
imports: list[type] = [] # Required module imports
exports: list[type] = [] # Public API types
controllers: list[type] = [] # Web controllers
is_global: bool = False # Exports visible to all modules
scan: list[str] = [] # Package paths to scan

The exports list (or the exports argument in dynamic modules) is the most critical part of a module. It defines which services are visible to other modules in the application.

  • Internal Services: Any service registered within a module but NOT exported is considered “private” and cannot be injected into components outside of that module.
  • Protocol Exports: It is best practice to export Protocols rather than implementation classes to maintain decoupling.
graph LR
    subgraph AuthModule
        AuthServiceImpl["AuthService (impl)"]
        AuthProtocol["AuthServiceProtocol (exported)"]
    end
    
    subgraph BillingModule
        BillingService["BillingService"]
    end
    
    BillingService --> AuthProtocol

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]

@global_module is @module(is_global=True). You do not also set is_global on the class.


Sometimes you need to configure a module dynamically before adding it to the application. Use the Module.configure() pattern for this.

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
)
from oridecon import Application
app = Application(name="my-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, **kwargs)Register additional providers in a per-feature scope
stub(config=None)Return a test-mode module with in-memory/noop backends

Module.scope() is already implemented on the base class. Use it to register extra providers without a second configure() (which would duplicate the module):

app.add_module(DatabaseModule.configure("postgresql://localhost/mydb"))
app.add_module(DatabaseModule.scope(UserRepository, OrderRepository))
@module()
class CacheModule(Module):
@classmethod
def stub(cls, config: Any = None) -> DynamicModule:
return DynamicModule(
module=cls,
providers=[FakeCacheProvider()],
exports=[CacheBackend],
)

@module()
class DataModule(Module):
@classmethod
async def on_module_booted(cls) -> None:
"""Called after all providers in this module have been booted.
Override in subclasses to run post-boot initialization logic.
The container is fully available at this point.
"""
pass
@classmethod
async def on_module_shutdown(cls) -> None:
"""Called before this module's providers are shut down.
Override in subclasses to run pre-shutdown cleanup logic.
"""
pass

The application maintains a global ModuleRegistry which tracks all loaded modules and their inter-dependencies. This ensures that:

  • Circular dependencies are caught early
  • Modules are initialized in the correct order
  • Module visibility rules are enforced

If a module tries to inject a service that is not in its imports’ exports (and is not global), Oridecon raises ModuleVisibilityError:

'TokenService' is not visible in 'BillingModule'
Requested by: BillingProvider
Exported by: AuthModule
BillingModule currently imports: []
Exported services available there: [AuthServiceProtocol]
To fix:
Fix one of:
1. Add the exporting module to BillingModule.imports
2. Make the exporting module global with @module(is_global=True)
3. Depend on a service that is already exported

src/my_app/modules/auth/__init__.py
from oridecon.di.module import module, Module
from my_app.modules.auth.provider import AuthProvider
from my_app.modules.auth.protocols import AuthServiceProtocol
@module(
providers=[AuthProvider],
exports=[AuthServiceProtocol], # Only the protocol is public
)
class AuthModule(Module):
"""Authentication — only AuthServiceProtocol is visible to importers."""

oridecon new module auth writes AuthModule. List it in create_app() next to WebModule — see Project Structure.

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

Start unscoped. Add a module when a feature needs a private interior and a public protocol — that is growth, not a different project type.

Terminal window
oridecon new module auth
oridecon gen controller users --module auth