Custom Extensions¶
waku's extension system lets you hook into the module and application lifecycle to implement
cross-cutting concerns — logging, validation, metrics, provider aggregation, and anything else
that does not belong inside a single module's business logic. Extensions are classes that
subclass one or more Protocol interfaces. A single class can implement several hooks by
inheriting from multiple protocols. All extension protocols are @runtime_checkable, so waku
can discover which hooks an extension supports at registration time.
There are two categories of extensions:
- Module extensions — attached to a specific module via
@module(extensions=[...])orDynamicModule(extensions=[...]). - Application extensions — passed to
WakuFactory(extensions=[...])and operate on the entire application.
Module extensions¶
Module extensions are placed in the extensions list of a module definition. They participate
in the module's own lifecycle.
The ModuleExtension type alias (defined in waku.extensions) captures all protocols
that can appear in a module's extensions list:
ModuleExtension: TypeAlias = (
OnModuleConfigure
| OnModuleInit
| OnModuleDestroy
| OnModuleRegistration
)
OnModuleConfigure¶
Invoked during @module() decoration (or DynamicModule metadata extraction),
before the module metadata is compiled into a Module object. The extension receives the
mutable ModuleMetadata and can add providers, imports, or exports.
Note
OnModuleConfigure runs synchronously because it executes at import time, inside the
@module() decorator. Do not perform I/O here — use OnModuleInit for async setup.
OnModuleInit¶
Called after the DI container is built, during application initialization. Modules are initialized in topological order (dependencies first), so a module can rely on its imported modules already being initialized.
OnModuleDestroy¶
Called during application shutdown in reverse topological order (dependents first), so a module's dependents are torn down before the module itself.
OnModuleRegistration¶
Runs after all module metadata has been collected but before Module objects
are created. Unlike OnModuleConfigure (which sees only its own module's metadata),
OnModuleRegistration receives the full ModuleMetadataRegistry and can perform cross-module
aggregation.
Key parameters:
| Parameter | Type | Purpose |
|---|---|---|
registry |
ModuleMetadataRegistry |
Read access to all modules; find_extensions() for discovery; add_provider() for contributing providers |
owning_module |
ModuleType |
The module that owns this extension instance — target for add_provider() calls |
context |
Mapping[Any, Any] | None |
Read-only application context passed to WakuFactory |
OnModuleRegistration can be used at both the module level and the application level.
Execution order:
- Application-level
OnModuleRegistrationextensions run first (assigned to the root module). - Module-level
OnModuleRegistrationextensions run next, in topological order.
The discovery pattern pairs two extensions: a data-carrying extension attached to feature modules and an aggregator extension that collects them during registration.
Application extensions¶
Application extensions are passed to WakuFactory and operate on the whole application.
The ApplicationExtension type alias (defined in waku.extensions) covers all
protocols accepted by WakuFactory(extensions=[...]):
ApplicationExtension: TypeAlias = (
OnApplicationInit
| AfterApplicationInit
| OnApplicationShutdown
| OnModuleRegistration
)
OnApplicationInit¶
Called during app.initialize(), after all OnModuleInit hooks have completed.
AfterApplicationInit¶
Called immediately after OnApplicationInit, once the application is fully
initialized and the container is available. This is the right place for validation,
health checks, or any logic that needs the complete, ready-to-use application.
The built-in ValidationExtension implements this protocol:
OnApplicationShutdown¶
Called during app.close(), after all OnModuleDestroy hooks have completed.
Built-in extensions¶
waku ships with a set of default application extensions. When you do not pass extensions=
to WakuFactory, these are used automatically.
See Validation for details on ValidationExtension and
writing custom validation rules.
# Framework-internal definition (for reference):
DEFAULT_EXTENSIONS = (
ValidationExtension(
[DependenciesAccessibleRule()],
strict=True,
),
)
Warning
When providing custom application extensions, you replace the defaults. To keep the
built-in validation, spread DEFAULT_EXTENSIONS into your list:
Combining multiple hooks¶
A single extension class can implement several protocols. This is most useful when setup and teardown logic are paired — a resource acquired on init must be released on shutdown:
Fluent builder pattern¶
Extensions that collect configuration benefit from a fluent builder API. The MediatorExtension
in waku's CQRS module is a good example — it chains .bind_request() and .bind_event() calls:
To support this pattern in your own extensions, return Self from configuration methods:
Real-world example: MediatorExtension¶
The CQRS mediator is the most comprehensive built-in extension. It combines two extension protocols across two classes:
MediatorExtension(OnModuleConfigure) — placed in each feature module. Collects request/event handler bindings via the fluent builder API.MediatorRegistryAggregator(OnModuleRegistration) — placed inMediatorModule. Discovers allMediatorExtensioninstances across the module tree, merges their registries, and contributes the aggregated providers to the appropriate modules.
- Create a fresh registry to merge all discovered handler bindings into.
- Walk every module that has a
MediatorExtensionattached. - Register each handler provider in the module that declared it.
- Collector providers (multi-bindings) go to the owning module (
MediatorModule). - Prevent further modifications to the registry.
- Make the aggregated registry itself available as a DI provider.
This pattern — marker extension for data collection + registration extension for aggregation — is the recommended approach for any cross-module discovery use case.
Further reading¶
- Lifecycle Hooks — full lifecycle diagram, hook reference table, and phase descriptions
- Application — application lifecycle and lifespan functions
- Modules — module system and the
@module()decorator - CQRS extension — the mediator extension in detail
- Advanced DI Patterns —
provider()helper and Dishka primitives forOnModuleRegistrationuse cases