# How Does the Module System Work in NestJS: A Deep Dive into the IoC Container Architecture

> Understand NestJS modules and their IoC container architecture. Learn how the framework uses decorators and reflection to build a runtime graph for dependency resolution. Dive deep into ModuleRef and ModulesContainer.

- Repository: [nestjs/nest](https://github.com/nestjs/nest)
- Tags: deep-dive
- Published: 2026-03-01

---

**NestJS modules are self-contained IoC containers that use the `@Module` decorator to store static metadata via reflection, which the framework compiles into a runtime graph of `Module` instances managed by `ModulesContainer` and accessed through `ModuleRef` for dependency resolution.**

Understanding how the module system works in NestJS is essential for building scalable, maintainable server-side applications. The `nestjs/nest` repository implements a sophisticated hierarchical dependency injection system where each module acts as an encapsulation boundary for providers, controllers, and exports. This architecture enables true separation of concerns while maintaining a powerful resolution mechanism that supports singleton, request-scoped, and transient lifecycles.

## Architectural Overview

NestJS organizes applications into **modules**, each functioning as an independent Inversion of Control (IoC) container. The system builds a runtime dependency graph from metadata supplied by the `@Module` decorator and manages it through four primary architectural components:

- **`@Module` decorator** – Stores static configuration (imports, providers, controllers, exports) on the target class using `Reflect.defineMetadata` at lines 22-27 of [`packages/common/decorators/modules/module.decorator.ts`](https://github.com/nestjs/nest/blob/main/packages/common/decorators/modules/module.decorator.ts)
- **`Module` class** – The runtime representation holding collections for imports, providers, injectables, controllers, and exports, alongside core providers like `ModuleRef` and `ApplicationConfig`
- **`ModulesContainer`** – A `Map<string, Module>` global registry that stores every instantiated module keyed by its unique UUID
- **`ModuleRef`** – The public API surface that controllers and services use to retrieve (`get`), resolve (`resolve`), or create (`create`) provider instances at runtime

## The Module Lifecycle: From Declaration to Runtime

### Declaration Phase: Metadata Storage via Reflection

When a developer applies the `@Module` decorator to a class, the framework stores the configuration object as metadata on that class constructor. In [`packages/common/decorators/modules/module.decorator.ts`](https://github.com/nestjs/nest/blob/main/packages/common/decorators/modules/module.decorator.ts), lines 22-27 implement this via `Reflect.defineMetadata`, capturing the `imports`, `providers`, `controllers`, and `exports` arrays for later compilation.

```typescript
import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';

@Module({
  imports: [],
  providers: [CatsService],
  controllers: [CatsController],
  exports: [CatsService],
})
export class CatsModule {}

```

### Bootstrap Phase: Container and Core Provider Registration

During application initialization, Nest creates a `ModulesContainer` instance. For every declared module class, the system instantiates a `Module` object in [`packages/core/injector/module.ts`](https://github.com/nestjs/nest/blob/main/packages/core/injector/module.ts), passing the class metatype and container reference. The constructor immediately invokes `addCoreProviders`, registering three essential services: the module itself, a `ModuleRef` created via `createModuleReferenceType`, and the shared `ApplicationConfig`.

### Compilation Phase: Building the Dependency Graph

The `ModuleCompiler` (referenced in the core injector system) scans the stored metadata and populates the `Module` instance's internal collections:

- `addProvider` registers class-based, value, factory, or existing providers
- `addInjectable` registers internal providers like guards and interceptors
- `addController` registers controller classes and assigns unique IDs via `assignControllerUniqueId`
- `addImport` links imported modules, while `addExportedProviderOrModule` exposes selected providers to consumers

Each provider is wrapped in an `InstanceWrapper` that tracks its **scope**—determined by `getClassScope` as `SINGLETON`, `REQUEST`, or `TRANSIENT`—and whether it is **durable** (`isDurable`), indicating if it should be cached across requests.

### Resolution Phase: Dependency Injection and Instantiation

When a controller requests a dependency, the `Injector` (utilized by `ModuleRef`) traverses the graph. The concrete `ModuleRef` implementation, created in `Module#createModuleReferenceType` (lines 66-78 of [`packages/core/injector/module.ts`](https://github.com/nestjs/nest/blob/main/packages/core/injector/module.ts)), exposes two primary retrieval methods:

- **`get(token, options?)`** – Performs a strict lookup within the host module's provider map by default; if `each: true` is specified, returns all matching instances
- **`resolve(token, contextId?)`** – Creates a new instance for request-scoped or transient providers, accepting an optional `ContextId` for request isolation

## Core Components Deep Dive

### The Module Class: Runtime State Management

Located in [`packages/core/injector/module.ts`](https://github.com/nestjs/nest/blob/main/packages/core/injector/module.ts), the `Module` class maintains the runtime state of each module. It tracks `isGlobal` status, module `distance` in the import hierarchy, and manages the `InstanceWrapper` collections. The class distinguishes between static modules and dynamic modules through the `isDynamicModule` check (lines 45-48), enabling configuration-driven module behavior.

### ModulesContainer: Global Registry Pattern

The `ModulesContainer` in [`packages/core/injector/modules-container.ts`](https://github.com/nestjs/nest/blob/main/packages/core/injector/modules-container.ts) extends `Map<string, Module>` and provides lookup utilities like `getById`. It serves as the single source of truth for all instantiated modules during the application lifecycle, enabling cross-module dependency resolution while maintaining encapsulation boundaries.

### ModuleRef: Public API for Runtime Access

Defined in [`packages/core/injector/module-ref.ts`](https://github.com/nestjs/nest/blob/main/packages/core/injector/module-ref.ts), this abstract class exposes the interface that developers interact with when accessing providers programmatically. When a service implements `OnModuleInit`, it can inject `ModuleRef` to retrieve collaborators that may not be available through standard constructor injection, respecting the **strict** (host-module only) and **each** (multi-instance) resolution options.

### InstanceWrapper and Scope Management

Every provider within a module is encapsulated in an `InstanceWrapper` that manages:
- The actual instantiated object or factory function
- Scope metadata (singleton vs. request-scoped vs. transient)
- Resolution state tracking to prevent circular dependency issues
- References to the host `Module` for hierarchical lookups

## Dynamic Modules and Configuration Patterns

NestJS supports **DynamicModules**—objects containing a `module` property plus optional `providers`, `imports`, and `exports`. This pattern enables configuration methods like `forRoot()` and `forFeature()`.

When the compiler encounters a dynamic module, it validates the structure via `isDynamicModule` and merges the dynamic metadata into the consuming module's graph:

```typescript
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static forRoot(options: { envFilePath: string }): DynamicModule {
    return {
      module: ConfigModule,
      providers: [{ provide: 'CONFIG_OPTIONS', useValue: options }, ConfigService],
      exports: [ConfigService],
    };
  }
}

```

## Global Modules and Cross-Cutting Providers

Marking a module with the `@Global()` decorator sets `module.isGlobal` to true, automatically adding the module to the imports of every other module without explicit declaration. This pattern is ideal for providing ubiquitous utilities like configuration services or database connections, though it should be used sparingly to avoid architectural coupling.

## Practical Implementation Examples

### Accessing Providers via ModuleRef

Services can programmatically retrieve dependencies using the `ModuleRef` API, particularly useful during lifecycle hooks:

```typescript
import { Injectable, OnModuleInit, ModuleRef } from '@nestjs/common';
import { CatsService } from './cats.service';

@Injectable()
export class ZooService implements OnModuleInit {
  private catsService: CatsService;

  constructor(private readonly moduleRef: ModuleRef) {}

  async onModuleInit() {
    this.catsService = this.moduleRef.get(CatsService);
  }

  getAllCats() {
    return this.catsService.findAll();
  }
}

```

### Resolving Request-Scoped Providers

For providers marked with `Scope.REQUEST`, use `resolve` to create a new instance per request context:

```typescript
import { Injectable, Scope, ModuleRef } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class RequestIdService {
  constructor(private readonly moduleRef: ModuleRef) {}

  async getId(contextId: any) {
    const idProvider = await this.moduleRef.resolve(RequestIdService, contextId);
    return idProvider.id;
  }
}

```

### Importing and Exporting Between Modules

When one module requires providers from another, explicit import and export declarations establish the dependency boundary:

```typescript
import { Module } from '@nestjs/common';
import { CatsModule } from './cats.module';
import { DogsService } from './dogs.service';

@Module({
  imports: [CatsModule],
  providers: [DogsService],
})
export class DogsModule {}

```

During compilation, `DogsModule`'s imports set contains the instantiated `CatsModule`, and `ModuleRef#get(CatsService)` resolves from the imported module's provider map when `DogsService` declares it as a dependency.

## Summary

- **Metadata-driven architecture**: The `@Module` decorator stores configuration via `Reflect.defineMetadata` in [`packages/common/decorators/modules/module.decorator.ts`](https://github.com/nestjs/nest/blob/main/packages/common/decorators/modules/module.decorator.ts), establishing the static structure that the compiler transforms into runtime objects.
- **Hierarchical IoC containers**: Each `Module` instance in [`packages/core/injector/module.ts`](https://github.com/nestjs/nest/blob/main/packages/core/injector/module.ts) acts as an isolated container with its own provider scope, while `ModulesContainer` maintains the global registry of all modules.
- **Flexible resolution strategies**: `ModuleRef` in [`packages/core/injector/module-ref.ts`](https://github.com/nestjs/nest/blob/main/packages/core/injector/module-ref.ts) provides both strict lookup (`get`) and factory-style instantiation (`resolve`) to handle singleton, request-scoped, and transient lifecycles.
- **Dynamic configuration support**: The module system supports `DynamicModule` objects enabling `forRoot()` and `forFeature()` patterns, validated through `isDynamicModule` checks during compilation.
- **Global provider sharing**: The `@Global()` decorator bypasses explicit imports by setting `isGlobal`, though this should be reserved for truly cross-cutting concerns to maintain modularity.

## Frequently Asked Questions

### How does NestJS resolve circular dependencies between modules?

NestJS uses forward references (`ForwardReference`) and the `ModuleRef` resolution mechanism to handle circular dependencies. When two modules import each other, the `ModuleCompiler` delays the resolution of specific providers until both modules are fully instantiated in the `ModulesContainer`. For provider-level circularity, the `InstanceWrapper` tracks resolution state to detect and manage circular constructor injection through property injection or lazy resolution via `ModuleRef.resolve()`.

### What is the difference between `ModuleRef.get()` and `ModuleRef.resolve()`?

According to the source code in [`packages/core/injector/module.ts`](https://github.com/nestjs/nest/blob/main/packages/core/injector/module.ts), `ModuleRef.get()` performs a strict lookup in the host module's provider map and returns the cached singleton instance by default, or all instances if `each: true` is passed. In contrast, `ModuleRef.resolve()` always creates a new instance for request-scoped or transient providers, allowing per-request isolation via an optional `ContextId` parameter. Use `get()` for standard dependency retrieval and `resolve()` when you need fresh instances or request-scoped behavior.

### How does the `isGlobal` flag affect module imports in `nestjs/nest`?

When a module is decorated with `@Global()`, the `isGlobal` property is set to true in the `Module` class constructor. During the compilation phase in [`packages/core/injector/module.ts`](https://github.com/nestjs/nest/blob/main/packages/core/injector/module.ts), the framework automatically adds this module to the imports collection of every other module in the application. This eliminates the need for explicit `imports: [GlobalModule]` declarations while still maintaining the module's encapsulation boundary for providers not listed in `exports`.

### Can I change a provider's scope after the module is compiled?

No, provider scope is determined at compilation time via `getClassScope` and stored immutably in the `InstanceWrapper` within [`packages/core/injector/module.ts`](https://github.com/nestjs/nest/blob/main/packages/core/injector/module.ts). The scope (`SINGLETON`, `REQUEST`, or `TRANSIENT`) and durability (`isDurable`) are fixed when the module graph is built. To change scope behavior, you must modify the `@Injectable({ scope: ... })` decorator on the provider class and restart the application, as the metadata is processed during the bootstrap phase before any instantiation occurs.