Skip to content

Dependency Injection

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

QuadKit 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 quadkit 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(CacheBackend) # 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, QuadKit will automatically resolve and inject the correct instance.

class ProductService:
def __init__(self, repo: ProductRepositoryProtocol) -> None:
# `repo` is resolved from the container by the constructor type hint
self.repo = repo
async def list_products(self) -> list[Product]:
return await self.repo.list_all()

Type-hint a protocol, not a concrete class. The container injects whatever implementation was bound in register().


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

DecoratorScopeImport From
@singletonOne instance for the appquadkit or quadkit.di
@injectableTransient by defaultquadkit or quadkit.di
@scopedOne instance per request/scopequadkit or quadkit.di
@transientNew instance each timequadkit or quadkit.di
from quadkit 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 __quadkit_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 quadkit.contracts.core.di import BootContainerProtocol
async def boot(self, container: BootContainerProtocol) -> None:
repo = await container.resolve(ProductRepositoryProtocol)
await repo.connect()

boot() takes BootContainerProtocol (resolve + bind()). After freeze, singleton() / transient() / scoped() raise ContainerError (QK_ERR_DI_001). See Container Protocols.


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 quadkit.di.markers 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.

Do not pass the container into a service (service locator). Type-hint the protocol on the constructor; the container wires it.

App providers generated by the CLI land in src/<app>/di/ (quadkit gen provider billing). Do not invent src/<app>/providers/ or models/.