Providers¶
Introduction¶
A provider tells waku how to create and deliver a dependency — what type it satisfies, how long it lives, and what it needs in order to be constructed. Providers are the building blocks of the module system: each module declares which providers it owns and which it exposes to other modules.
Dependency Injection¶
waku provides a module system that lets you organize providers into cohesive, self-contained units with explicit import/export boundaries. At bootstrap, waku collects providers from all modules, resolves the module dependency graph, and hands the result to the Dishka IoC container, which handles dependency resolution and lifecycle management.
What is Dependency Injection?
Dependency Injection (DI) is a design pattern that addresses the issue of tightly coupled code by decoupling the creation and management of dependencies from the classes that rely on them. In traditional approaches, classes directly instantiate their dependencies, resulting in rigid, hard-to-maintain code. DI solves this problem by enabling dependencies to be supplied externally, typically through mechanisms like constructor or setter injection.
By shifting the responsibility of dependency management outside the class, DI promotes loose coupling, allowing classes to focus on their core functionality rather than how dependencies are created. This separation enhances maintainability, testability, and flexibility, as dependencies can be easily swapped or modified without altering the class's code. Ultimately, DI improves system design by reducing interdependencies and making code more modular and scalable.
See also: Dishka — Introduction to DI
Manual DI Example
Here, a MockClient is injected into Service, making it easy to test Service in isolation without relying
on a real client implementation.
What is IoC-container?
An IoC container is a framework that automates object creation and dependency management based on the Inversion of Control (IoC) principle. It centralizes the configuration and instantiation of components, reducing tight coupling and simplifying code maintenance. By handling dependency resolution, an IoC container promotes modular, testable, and scalable application design.
With the power of an IoC container, you can leverage all the benefits of DI without manually managing dependencies.
See also: Dishka — Key Concepts
Providers¶
A Provider is a class that holds dependency metadata, such as its type, lifetime scope and factory.
In waku, there are five provider helpers:
Each provider (except object_() and contextual()) accepts two positional arguments:
interface_or_source: the type to register — or the interface type when a separate implementation is provided.implementation(optional): the implementation type or factory. When given, the first argument is treated as the interface.
Source types¶
Every provider helper accepts classes and callables as the source of a dependency. The container inspects the source's type hints to determine what to inject.
Classes¶
The most common source — the container calls the constructor and injects parameters:
Factory functions¶
Any callable with a return type annotation works as a source. The container injects the function's parameters and uses the return value:
Async factory functions¶
Async factories work the same way — the container awaits them during resolution:
Generator factories¶
Generator factories support resource cleanup. Yield the dependency; code after
yield runs when the scope exits:
Async generators follow the same pattern with AsyncIterator:
Working with context managers
Do not apply @contextmanager or @asynccontextmanager to generator
factories — Dishka manages the generator lifecycle directly and these
decorators interfere with that mechanism. Use plain generators as shown above.
To wrap an existing context manager class, enter it inside a generator factory:
For automatic enter/exit of all context manager dependencies without writing
individual wrappers, see dishka#457
for a @decorate-based recipe.
Scopes¶
Provider helper names are inspired by .NET Core's service lifetimes. Under the hood, each helper maps to a Dishka scope that determines the dependency's lifetime. Dishka uses two primary scopes:
APP— the dependency lives for the entire application lifetime.REQUEST— the dependency lives for a single scope entry (e.g., one HTTP request).
Dependencies are lazy — they are created when first requested. Within a scope, the same instance is returned by default (configurable per helper). When a scope exits, all its dependencies are finalized in reverse creation order.
For more details, see the Dishka scopes documentation.
Transient¶
Registers the dependency in Scope.REQUEST with caching disabled.
A new instance is created every time the dependency is requested, even within the same scope.
Scoped¶
Registers the dependency in Scope.REQUEST with caching enabled.
The instance is created once per scope entry and reused for all subsequent requests within that scope.
Finalized when the scope exits.
Singleton¶
Registers the dependency in Scope.APP with caching enabled.
The instance is created once on first request and reused across all scopes for the entire application lifetime.
Finalized when the application shuts down.
Object¶
Registers a pre-created instance in Scope.APP.
Unlike singleton(), you provide the instance directly — its lifecycle is managed by you, not the container.
Contextual¶
contextual() injects external objects — values that originate outside the DI container
and have their own lifecycle (HTTP requests, database transactions, event data).
When to use:
- Framework integration: HTTP request objects, user sessions, authentication contexts
- Event-driven scenarios: Queue messages, webhooks, callback data
- External resources: Database transactions, file handles, network connections managed by external systems
- Per-request data: Any data that varies per request/operation and originates from outside your application
How it works:
- Declare the contextual dependency using the
contextualprovider in your module - Use the dependency in other providers just like any regular dependency
- Provide the actual value when entering the container scope using the
context=parameter. Context can be provided at APP level viaWakuFactory(context=...)or at REQUEST level viaapp.container(context=...)— see Application — Container Access for details
contextual() accepts two arguments:
provided_type: the type of the dependency to be injected.scope: the scope where the context is available (defaults toScope.REQUEST).
FastAPI example — injecting the current request into your service layer:
Important
In this example, the contextual provider and waku itself are used to manually inject the current request into the UserService.
However, in real-world applications, you should use the Dishka FastAPI integration to inject the request automatically.
Where and how to inject dependencies?¶
To inject dependencies with waku, you need to:
- Register them as
providerswith the desired scope in modules. - Identify your application entrypoints and decorate them with the
@injectdecorator for your framework. Consult the Dishka integrations section for your framework to find out how to do this. - Add dependencies as arguments to your entrypoint signature using the
Injectedtype hint.
Further reading¶
- Modules — module system, imports, and exports
- Advanced DI — conditional providers, multi-bindings, and Dishka primitives
- Dishka documentation — the underlying DI framework