-
-
Notifications
You must be signed in to change notification settings - Fork 8k
Description
Is there an existing issue that is already proposing this?
- I have searched the existing issues
Is your feature request related to a problem? Please describe it
NestJS ensures a topological instantiation order for providers based on their dependency graph (i.e., new MyProvider(...)
respects dependencies). However, this ordering is not enforced for asynchronous lifecycle hooks, such as:
onModuleInit
(initialization)onModuleDestroy
(teardown)
Currently, all providers within the same module are initialized and destroyed concurrently (Promise.all
), disregarding their dependency relations:
https://github.com/nestjs/nest/blob/master/packages/core/hooks/on-module-init.hook.ts#L50
This is surprising because modules are initialized sequentially in topological order, ensuring a safeguard against race conditions. The "distance" terminology likely follows BFS levels, akin to Khan's algorithm:
https://github.com/nestjs/nest/blob/master/packages/core/nest-application-context.ts#L405-L408
it('should sort modules by distance (topological sort) - DESC order', async () => {
Describe the solution you'd like
Feature Request
Lifecycle hooks within a module should execute sequentially in a topological (for init) and reverse-topological (for teardown) order to ensure:
- During initialization: A provider’s dependencies have fully initialized (
onModuleInit
resolved) before its ownonModuleInit
executes. - During teardown: A provider’s dependents have fully torn down (
onModuleDestroy
resolved) before its ownonModuleDestroy
executes.
This would improve lifecycle orchestration and prevent potential race-conditions.
Teachability, documentation, adoption, migration strategy
Ideally, the Lifecycle Hooks documentation will explicitly clarify the following points:
- Module Initialization Order: Modules are initialized sequentially in topological order during the init phase and reverse-topological order during teardown.
- Provider Initialization: Within each module, providers'
onModuleInit
hooks are invoked sequentially in topological order. - Provider Teardown: Within each module, providers'
onModuleDestroy
hooks are invoked sequentially in reverse-topological order.
This clarification will help developers better understand the precise execution flow of lifecycle hooks.
What is the motivation / use case for changing the behavior?
Comparison with Other Popular Dependency-Injection Framework
My suggestion aligns with Spring's default behavior:
https://docs.spring.io/spring-framework/reference/core/beans/factory-nature.html
"The order of startup and shutdown invocations can be important. If a "depends-on" relationship exists between any two objects, the dependent side starts after its dependency, and it stops before its dependency".
Use Case Example
Consider a module with two providers:
- A Kafka client provider that establishes a connection in
onModuleInit()
. - A Kafka publisher provider (built on top of the client) that creates a test topic in
onModuleInit()
.
Issue
Since both providers belong to the same module, Nest does not guarantee a topological execution order for their lifecycle hooks. This leads to potential failures:
- Initialization (
onModuleInit
): The publisher may attempt to publish before the client has established a connection. - Teardown (
onModuleDestroy
): The publisher may attempt to send messages after the client has disconnected.
Current Approach to Mitigate the Issue
Developers can implement custom lifecycle coordination layers, but this undermines NestJS’s built-in orchestration capabilities. A practical workaround is to place each provider with an asynchronous onModuleInit
or onModuleDestroy
hook into its own dedicated module - effectively creating one-provider modules.
However, this approach introduces significant downsides: excessive modularity and fragmentation, as logically related providers become scattered across multiple modules.