Skip to content
GitHubDiscord

Dependency Injection

Lexigram is built on a powerful, lightweight Dependency Injection (DI) container. Instead of your components creating their own dependencies, they are “injected” at runtime, leading to cleaner code, easier testing, and true modularity.

The DI container (Inversion of Control) is the central registry where all application services live. You interact with it primarily through Providers or by resolving services directly from the Application instance.

Lexigram supports three primary registration lifetimes:

ScopeMethodDescription
Singletoncontainer.singleton()Only one instance is created for the entire application lifecycle.
Scopedcontainer.scoped()A new instance is created per request/operation (common in Web controllers).
Transientcontainer.transient()A new instance is created every time the dependency is requested.
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.scoped(DbSession, SqlAlchemySession) # one per scope
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
# Scoping
async with container.scope() as scoped:
session = await scoped.resolve(DbSession) # scoped to this block

This is the preferred way to handle dependencies. By simply type-hinting your constructor parameters with a Protocol or Class, Lexigram will automatically resolve and inject the correct instance.

from lexigram.contracts.data.db import DatabaseProviderProtocol
class ProductService:
def __init__(self, db: DatabaseProviderProtocol):
# The 'db' instance is automatically resolved from the container
self.db = db
async def list_products(self):
return await self.db.query("SELECT * FROM products")

Lexigram provides decorators to mark classes for automatic discovery and registration:

DecoratorScopeImport From
@singletonOne instance for the applexigram or lexigram.di
@injectableTransient by defaultlexigram or lexigram.di
@scopedOne instance per request/scopelexigram or lexigram.di
@transientNew instance each timelexigram or lexigram.di
from lexigram import singleton, injectable, scoped
@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
@scoped
class RequestContext:
def __init__(self) -> None:
self.request_id = generate_id()
  1. @singleton marks GreetingService with __lexigram_injectable__ metadata
  2. Application.discover_providers() scans the package and finds the marked class
  3. At boot, the container registers GreetingService as a singleton
  4. When HelloController is instantiated, the container resolves GreetingService from the constructor type hints

While constructor injection is preferred, you can also resolve dependencies manually from the container when necessary.

from lexigram.contracts.core.di import ContainerResolverProtocol
# Within a provider boot() method
async def boot(self, container: ContainerResolverProtocol):
db = await container.resolve(DatabaseProviderProtocol)
await db.connect()

For boot() methods, use BootContainerProtocol to also allow service registration. See Container Protocols for the full protocol hierarchy.


You can register services with names for more granular resolution:

container.singleton(
CacheBackend,
RedisCacheBackend(),
name="redis"
)
# Resolve by name using Annotated
from typing import Annotated
from lexigram.di.annotations import Named
cache = await container.resolve(Annotated[CacheBackend, Named("redis")])

In testing scenarios, you can override service registrations:

container = Container(testing_mode=True)
# Override with fake
container.override(UserRepository, FakeUserRepository())

Note: override() is only available in containers created with testing_mode=True.