Skip to content
GitHubDiscord

Core Concepts

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 app

Key methods on Application:

MethodPurpose
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 form
async with Application.boot(
name="my-app",
providers=[AppProvider(), WebProvider()],
config=config,
) as app:
# app is started and ready
pass

Providers register services in the DI container and manage their lifecycle. Every provider follows a two-phase pattern:

from lexigram.di.provider import Provider
from lexigram.contracts.core import ProviderPriority
from 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()

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

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_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 lexigram 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 applexigram
@injectableTransient by defaultlexigram
@scopedOne instance per request/scopelexigram
@transientNew instance each timelexigram
@injectEnable DI on async functionslexigram
from lexigram 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()
...

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

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."""

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:

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 lexigram.di.module import global_module, Module
@global_module
class LoggingModule(Module):
providers = [LoggingProvider]
exports = [LoggerProtocol]
Without Modules (Pattern 2)With Modules (Pattern 3)
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
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  • Configuration β€” YAML config, environment profiles, and auto-injection
  • lexigram-web β€” Controllers, routing, and middleware