Core Concepts
Application
Section titled βApplicationβThe Application class is the composition root. It manages providers, modules, configuration, and lifecycle.
from lexigram import Application, LexigramConfig
def create_app() -> Application: config = LexigramConfig.from_yaml() app = Application(name="my-app", config=config) # Wire services here... return appKey methods on Application:
| Method | Purpose |
|---|---|
add_provider(provider) | Register a Provider instance |
add_module(module) | Register a @module class or DynamicModule |
discover_providers(*packages) | Scan packages for Provider subclasses and @injectable classes |
start() | Boot all providers (register β freeze β boot) |
stop() | Shutdown in reverse order |
Application.boot(...) | Context manager β start(), yield, stop() |
# Context manager formasync with Application.boot( name="my-app", providers=[AppProvider(), WebProvider()], config=config,) as app: # app is started and ready passProviders
Section titled βProvidersβProviders register services in the DI container and manage their lifecycle. Every provider follows a two-phase pattern:
from lexigram.di.provider import Providerfrom lexigram.contracts.core import ProviderPriorityfrom lexigram.contracts.core.di import ( ContainerRegistrarProtocol, ContainerResolverProtocol,)
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.domain.services import UserService
container.singleton(UserService, UserService)
async def boot(self, container: ContainerResolverProtocol) -> None: """Phase 2: Initialize resources. Resolving is now safe.""" service = await container.resolve(UserService) await service.warmup_cache()Provider Priorities
Section titled βProvider PrioritiesβProviders boot in ascending order. Lower values run first:
| Priority | Value | Purpose | Example |
|---|---|---|---|
CRITICAL | 0 | Absolutely foundational | Config, diagnostics |
INFRASTRUCTURE | 10 | Low-level plumbing | Database, cache, message brokers |
SECURITY | 20 | Auth infrastructure | Auth, encryption |
NORMAL | 30 | Everyday services (default) | Generic services |
APPLICATION | 40 | Application-level tools | CLI, admin utilities |
DOMAIN | 50 | Business logic | Your domain providers |
PRESENTATION | 80 | Entry points | WebProvider |
COMMS | 90 | Outbound communication | Email, SMS, webhooks |
LOW | 100 | Optional, boot last | Plugins, analytics |
Config Auto-Injection
Section titled βConfig Auto-Injectionβ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 LexigramConfig.get_section(config_key, config_model) before register() and assigns the result to provider.config.
Provider Lifecycle Hooks
Section titled βProvider Lifecycle Hooksβ| Hook | Phase | When Called |
|---|---|---|
register(container) | Registration | Container open for bindings |
boot(container) | Boot | Container frozen, resolution allowed |
shutdown() | Shutdown | Application stopping |
on_error(error, phase) | Error | When boot() or shutdown() raises |
health_check(timeout) | Health | Aggregated by Application.health_check() |
Dependency Injection
Section titled βDependency InjectionβLexigram 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_busContainer API
Section titled βContainer APIβ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 lexigram 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.singleton(CacheBackend, factory=create_redis) # lazy factorycontainer.scoped(DbSession, SqlAlchemySession) # one per scope (class as factory)container.transient(RequestContext, RequestContext) # new instance each resolve
# Resolutionservice = await container.resolve(PaymentGateway) # returns the StripeGateway instanceoptional = await container.resolve_optional(EventBus) # None if not registeredall_impls = await container.resolve_all(BaseHandler) # all subtypessync_val = container.resolve_sync(UserService) # sync (instantiated singletons only)
# Scopingasync with container.scope() as scoped: session = await scoped.resolve(DbSession) # scoped to this block
# Lifecyclecontainer.freeze() # no more registrations allowedcontainer.override(Service, fake) # testing_mode=True onlyawait container.dispose() # cleanup all singletonsDI Decorators
Section titled βDI Decoratorsβ| Decorator | Scope | Import From |
|---|---|---|
@singleton | One instance for the app | lexigram |
@injectable | Transient by default | lexigram |
@scoped | One instance per request/scope | lexigram |
@transient | New instance each time | lexigram |
@inject | Enable DI on async functions | lexigram |
from lexigram import singleton, injectable, inject
@singletonclass ConfigService: def __init__(self) -> None: self.settings = load_settings()
@injectable # transient by defaultclass UserService: def __init__(self, config: ConfigService) -> None: self.config = config
@injectasync def handle_request(user_svc: UserService) -> None: # user_svc resolved via container.call() or Invoker.invoke() ...Result Type
Section titled βResult TypeβLexigram uses Result[T, E] for operations that can fail β no exceptions for expected errors.
from lexigram.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)Working With Results
Section titled βWorking With Resultsβresult = await service.find_user("user-42")
# Check + unwrapif result.is_ok(): user = result.unwrap()if result.is_err(): error = result.unwrap_err()
# Safe accessuser = result.unwrap_or(default_user)user = result.unwrap_or_else(lambda e: create_fallback(e))
# Pattern matchingmessage = 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)
# Filteringvalid = result.filter(lambda u: u.is_active, InactiveUserError())
# Side effects without transformationresult.inspect(lambda u: logger.info("found", user=u.id))result.inspect_err(lambda e: logger.warning("failed", error=str(e)))
# Nestingnested: Result[Result[str, E], E] = Ok(Ok("hello"))flat = nested.flatten() # β Ok("hello")
# Bridge from exceptionstry: data = json.loads(raw)except ValueError as e: return Result.from_exception(e)Utilities
Section titled βUtilitiesβfrom lexigram.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)Modules
Section titled βModulesβFor larger projects (Pattern 3), modules add encapsulation boundaries over providers. Services inside a module are private by default β only explicitly exported types are visible to importers.
from lexigram.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."""Dynamic Modules
Section titled βDynamic ModulesβFor infrastructure that needs runtime configuration, override configure() on the Module base class:
from lexigram.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:
| Method | Purpose |
|---|---|
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 |
Global Modules
Section titled βGlobal ModulesβA @global_moduleβs exports are visible to all modules without explicit import:
from lexigram.di.module import global_module, Module
@global_moduleclass LoggingModule(Module): providers = [LoggingProvider] exports = [LoggerProtocol]Why Modules?
Section titled βWhy Modules?β| Without Modules (Pattern 2) | With Modules (Pattern 3) |
|---|---|
| Every service is globally visible | Services are private by default |
| Any class can depend on any other | Only exported protocols are accessible |
| No encapsulation boundaries | ModuleCompiler validates the import/export graph at boot |
Application Lifecycle
Section titled βApplication Lifecycleβββββββββββββββββββββββββββββββββββββββββββββ 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βββββββββββββββββββββββββββββββββββββββββββNext Steps
Section titled βNext Stepsβ- Configuration β YAML config, environment profiles, and auto-injection
- lexigram-web β Controllers, routing, and middleware