Good application modularity is closely related to the injector tree hierarchy that Dependency Injection (DI) creates. This article uses NestJS v10.0 and Ditsmod v2.38 for comparison. I am the author of . Ditsmod DI injectors are sometimes referred to as DI containers, but since and have borrowed many concepts from , this post will use the term "injectors" as it does in Angular. NestJS Ditsmod Angular Scopes in NestJS In NestJS, there are no explicitly defined levels of the DI injector hierarchy, but there are scopes that implicitly refer to such a hierarchy: - A single instance of the provider is shared across the entire application. The instance lifetime is tied directly to the application lifecycle. Once the application has bootstrapped, all singleton providers have been instantiated. Singleton scope is used by default. DEFAULT - A new instance of the provider is created exclusively for each incoming request. The instance is garbage-collected after the request has completed processing. REQUEST - Transient providers are not shared across consumers. Each consumer that injects a transient provider will receive a new, dedicated instance. TRANSIENT It appears that NestJS v10.0 does not yet have the ability to instantiate providers at the module level. This leads to a degradation of the modularity of applications: ; Exception filters module-scoped ; Can I use Interceptor in Module? What should I do? . Allow APP_* providers to be module-scoped rather than global DI Injector Hierarchy in Ditsmod Ditsmod has 4 static levels of the DI injector hierarchy: Application level. Providers are instantiated only once during the application's life cycle (this is the equivalent of the scope in NestJS, but in Ditsmod, this is instantiated on the first request, while in NestJS, the instance is instantiated at application startup); DEFAULT Module level. The provider instance is created once for each module; Route level. The provider instance is created once for each route; HTTP request level. A provider instance is created once for each HTTP request (this is the equivalent of the scope in NestJS). REQUEST In addition, at each of these levels, Ditsmod has the ability to create a new instance of a particular provider each time without using the injector cache (this is the equivalent of the scope in NestJS). TRANSIENT Features of Controllers in NestJS By default, NestJS instantiates a controller as a singleton. While this feature can improve application performance by several percent, it also increases the likelihood of "shooting yourself in the foot" when the developer creates a property in the controller for a particular HTTP request: @Controller() export class CatsController { private propertyWithRequestContext: any; @Get() method1() { // Works with this.propertyWithRequestContext } } Now, if 10 requests come to , they will all overwrite and interfere with each other. method1() propertyWithRequestContext In addition, if a default-scoped controller has a dependency on a request-scoped service, then such a controller automatically (without warning) becomes request-scoped as well. In this way, the developer cannot rely on properties in the controller at all because it is not clear what scope the controller will have as a result. Also, if the service is transient-scoped, then a similar scope change will not occur in the controller, introducing additional inconsistency in the NestJS architecture. In NestJS v9.0, it became possible to create so-called to have request-scoped services and not have to rebuild the dependency tree for every request. durable providers Judging from my tests, such services work almost as slowly as regular request-scoped services, but another complication of the NestJS application architecture has been added. Features of Controllers in Ditsmod A controller instance is created for each HTTP request. Regardless, performance with this controller is about the same as NestJS + Fastify with the default scope. When an application-level service tries to get a request-scoped service, Ditsmod throws an error that the service is not found (the injector higher in the hierarchy does not see its child injectors). Getting Current Injector The NestJS documentation : says Occasionally, you may want to resolve an instance of a request-scoped provider within a . Let's say that is request-scoped and you want to resolve the instance which is also marked as a request-scoped provider. In order to share the same DI container sub-tree, you must obtain the current context identifier instead of generating a new one request context CatsService CatsRepository In the following example, the method compares the instance that NestJS returns in the constructor with the instance returned by the method. If NestJS uses the same injector in both cases, should return : isSameInjector() CatsRepository this.moduleRef.resolve() isSameInjector() true import { REQUEST, ModuleRef, ContextIdFactory } from '@nestjs/core'; import { CatsRepository } from './cats-repository'; @Injectable() export class CatsService { constructor( @Inject(REQUEST) private request: Record<string, unknown>, private moduleRef: ModuleRef, private catsRepository: CatsRepository ) {} async isSameInjector() { const contextId = ContextIdFactory.getByRequest(this.request); const catsRepository = await this.moduleRef.resolve(CatsRepository, contextId); return catsRepository === this.catsRepository; } } In Ditsmod, the same thing can be done much easier because if and are request-scoped, they share the same injector: CatsService CatsRepository import { injectable, Injector } from '@ditsmod/core'; import { CatsRepository } from './cats-repository'; @injectable() export class CatsService { constructor( private injector: Injector, private catsRepository: CatsRepository ) {} isSameInjector() { const catsRepository = this.injector.get(CatsRepository); return catsRepository === this.catsRepository; } } Conclusion If you like NestJS, chances are you'll like Ditsmod more. Also published here