Why do we use dependency injection?
When you’re developing a smaller component of your system, such as a module or a class, you often require functionality from other classes. For instance, you might need an HTTP service for making backend calls. Dependency Injection (DI) is a design pattern and mechanism that facilitates the creation and provision of certain application parts to other parts of the application that need them. DI allows you to decouple components and manage their dependencies more efficiently.
Why do we have separate modules?
Feature Module:
A feature module serves to organize code relevant to a specific feature, aiding in code organization and establishing clear boundaries. This practice helps manage complexity and encourages adherence to SOLID principles.Shared Module:
A shared module is a modular component housing features, functionalities, or services intended for use across different parts of the application. It facilitates the organization and encapsulation of code that may be reused throughout the application, such as utility functions, common services, or custom decorators.Global Module:
A global module is a specialized module providing services or functionality intended to be available throughout the entire application. These modules typically offer application-wide services like logging, configuration, database connections, or authentication services.
To denote a global module in NestJS, the @Global()
decorator from the @nestjs/common package is employed. Decorating a module class with @Global()
signifies to NestJS that the module and its provided services should be globally accessible throughout the application, eliminating the need for explicit imports in the modules where they are utilized.
Providers
Providers in NestJS serve as services or dependencies that can be injected into other components such as controllers, other services, or custom decorators.
NestJS initializes providers and resolves their dependencies during the startup phase of your application. This process occurs when your application begins running.
When you import modules containing providers, NestJS instantiates these providers and stores them in its built-in Dependency Injection (DI) container. Before creating a new instance, the DI container checks if an instance of the service already exists. Throughout the application’s lifecycle, the DI container maintains a registry of all providers and their instances. It manages the lifecycle of these instances, including their creation, resolution, and disposal.
In scenarios of nested service calls, where a service method is invoked within another service method, NestJS consistently utilizes the same instance of the service. This approach ensures that any modifications to the service’s state within the nested call hierarchy are consistently reflected throughout the application.
The @Injectable() decorator marks a class as a provider in NestJS. By applying @Injectable() to a class, you enable the injection of instances of that class into other components using constructor injection or property injection.
import { Injectable } from '@nestjs/common';
import { ExampleService } from './example.service';
@Injectable()
export class ExampleController {
constructor(private readonly exampleService: ExampleService) {}
// Controller logic...
}
Scope in NestJS determines the lifecycle and availability of providers. There are three primary scopes:
Singleton: This is the default scope where a single instance of a provider is shared across the entire application. Any component that injects this provider will receive the same instance.
Transient: In this scope, a new instance of a provider is created each time it is injected. This ensures that each component receives a fresh instance of the provider.
Request: Here, a new instance of a provider is created for each incoming HTTP request. This instance is available only within the scope of that particular request. Once the request is completed, the instance is discarded.
Issue Encountered with Dependency Service in NestJS Application
The problem arose when utilizing a configuration service responsible for providing config values throughout the application. Various services from different modules relied on this configuration service. Employing Dependency Injection (DI) enabled this service to be injectable with a default scope, typically singleton, meaning the same instance persisted throughout the application’s lifecycle. However, two distinct methods were employed to integrate this service into different modules.
One approach involved including the configuration service in the import array, as defined in Module C. Conversely, in Module B, the service was added to the providers array. This distinction led to a discrepancy: when updating the configuration service, the services within Module C received the latest configuration, while those within Module B retained outdated configuration values.
Understand the issue with an example of the code:
service A
import { Injectable} from '@nestjs/common';
@Injectable()
export class UserService {
private user: string = '';
getUser() {
return this.user;
}
updateUser() {
const x = ... // db operation to get the value of x
this.user = x;
}
}
module A
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
@Module({
providers: [UserService],
exports: [UserService], // Export UserService for injection
})
export class UserModule {}
module B
import { Module } from '@nestjs/common';
import { UserService } from '../user/user.service';
@Module({
providers: [UserService],
})
export class ModuleB {}
module C
import { Module } from '@nestjs/common';
import { UserModule } from '../user/user.module';
@Module({
imports: [UserModule],
})
export class ModuleC {}
Why was the above scenario occurring?
Every time you add a provider to a providers array, you’re telling Nest to create that provider in the current module’s context. That means a new instance is getting created, and different instances can have different values, which are causing this issue. If you only want one instance (usually the case), you just need to import the module that originally exports the provider.
How it typically works:
Creation of Provider Services: When you define a provider using the @Injectable() decorator, NestJS creates an instance of that provider and registers it within the DI container. This instance will be reused whenever it is requested within the same module context unless you specify otherwise (e.g., if you’re using a provider with the @Scope()
decorator).
Disposal of Provider Services: The disposal of provider services typically happens when the application shuts down. NestJS handles the cleanup of resources automatically, so you generally don’t need to worry about explicitly disposing of services.
Updates on Class Properties: If you have class properties that get updated dynamically, you need to be careful about the scope and lifecycle of your provider instances. Changes to class properties will affect the instance of the class within the scope where it’s instantiated. If a provider is singleton-scoped (which is the default scope in NestJS), changes to its properties will affect all consumers within the same application instance.
Module Context: In NestJS, modules provide a way to organize the application into cohesive blocks of functionality. Each module has its own DI container, which means that providers registered within a module are scoped to that module. Therefore, changes to a provider’s properties will only affect other providers or components within the same module.
The instance created by providing it in the providers array of that module is scoped to the module and is independent of the singleton instance at the application level because they are separate instances with their own memory space. Any updates made to the properties of the service instance will not affect the instance in the context of the module.
Solution:
Using the global decorator, you don’t need to import it everywhere it’s required, as it is available throughout the application.
Import it in the module instead of providing it in the providers array.
Precautions:
Make sure you’re not redefining the service within the module using the providers array.
Make sure you’re not inadvertently importing or providing the service multiple times within the module or its parent modules.