Skip to content

NestJS Architecture -- Deep Dive

NestJS enforces modular architecture through its module/controller/service pattern with a compile-time dependency injection container. Its layered execution pipeline (middleware, guards, interceptors, pipes, exception filters) mirrors Spring's filter chain but runs on Node.js. First-class WebSocket gateway support and tight BullMQ integration make it the right choice for Warmwind's real-time VNC relay and container orchestration backend.


1. Module System & Dependency Injection

Every NestJS application is a tree of modules. Each module declares its controllers, providers (services), imports, and exports -- forming explicit dependency boundaries that the DI container wires at bootstrap time.

graph LR
    AppModule --> AuthModule
    AppModule --> ContainerModule
    AppModule --> AgentModule
    AppModule --> HealthModule
    ContainerModule --> DockerService
    ContainerModule --> VncGateway
    ContainerModule --> BullQueue["BullMQ Queue (Redis)"]
    AgentModule --> AiInferenceService
    AgentModule --> AgentEventsGateway
    AuthModule --> JwtStrategy
    AuthModule --> TenantService

1.1 Module Anatomy

A module is a class decorated with @Module(). It is the compilation unit of DI -- providers declared in one module are invisible to another unless explicitly exported and imported.

import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bullmq';
import { ContainerController } from './container.controller';
import { ContainerService } from './container.service';
import { ContainerProcessor } from './container.processor';
import { VncGateway } from './vnc.gateway';
import { DockerService } from './docker.service';

@Module({
  imports: [
    BullModule.registerQueue({ name: 'containers' }),
  ],
  controllers: [ContainerController],
  providers: [
    ContainerService,
    ContainerProcessor,
    VncGateway,
    DockerService,
  ],
  exports: [ContainerService],  // visible to importing modules
})
export class ContainerModule {}

1.2 Provider Scopes

Scope Lifetime Analogy (Spring) Use Case
SINGLETON One instance for the entire app (default) @Scope("singleton") Stateless services, DB repositories
REQUEST New instance per incoming HTTP request or GraphQL operation @Scope("request") Multi-tenant context, per-request audit trail
TRANSIENT New instance every time it is injected @Scope("prototype") Stateful helpers, unique per-consumer loggers
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';

@Injectable({ scope: Scope.REQUEST })
export class TenantService {
  private readonly tenantId: string;

  constructor(@Inject(REQUEST) private readonly request: Request) {
    // Resolved once per HTTP request -- the constructor is your
    // "request-scoped initializer," similar to Spring's request bean proxy.
    this.tenantId = this.request.headers['x-tenant-id'] as string;
  }

  getTenantId(): string {
    return this.tenantId;
  }
}

REQUEST scope bubbles up

If a SINGLETON depends on a REQUEST-scoped provider, the singleton silently becomes request-scoped too. This can multiply object allocations by orders of magnitude under load. Audit the scope chain with nest info and keep request-scoped providers at leaf positions. Prefer passing tenant context through a ClsModule (continuation-local storage) to avoid scope bubbling entirely.

Coming from Spring

NestJS DI is Spring's @Autowired + @Configuration compressed into decorators. Module = @Configuration class. Provider = @Bean method or @Component-scanned class. exports array = making a bean visible outside its configuration boundary. The key difference: Spring scans packages automatically by default; NestJS requires explicit imports, which eliminates "magic bean" surprises but demands more wiring discipline.

1.3 Custom Providers & Factory Patterns

When you need runtime logic to create a provider (environment-dependent config, feature flags), use factory providers:

const DockerProvider = {
  provide: 'DOCKER_CLIENT',
  useFactory: (config: ConfigService) => {
    const socketPath = config.get<string>('DOCKER_SOCKET', '/var/run/docker.sock');
    return new Dockerode({ socketPath });
  },
  inject: [ConfigService],
};

@Module({
  providers: [DockerProvider, ContainerService],
  exports: ['DOCKER_CLIENT'],
})
export class DockerModule {}
// Consuming the factory-provided token
@Injectable()
export class ContainerService {
  constructor(
    @Inject('DOCKER_CLIENT') private readonly docker: Dockerode,
  ) {}
}

Coming from Spring

useFactory is the exact equivalent of a @Bean factory method inside a @Configuration class. useClass maps to component scanning. useValue maps to @Value-injected constants. NestJS just makes the registration explicit rather than annotation-driven.


2. Execution Pipeline

Every HTTP request (or WebSocket frame, or GraphQL operation) passes through a strict, ordered pipeline. Understanding this pipeline is essential for placing cross-cutting logic correctly.

graph LR
    MW["1 Middleware"] --> GD["2 Guards"] --> INT_PRE["3 Interceptors (pre)"] --> PP["4 Pipes"] --> H["5 Route Handler"] --> INT_POST["6 Interceptors (post)"] --> EF["7 Exception Filters"]

2.1 Middleware

Runs first. Has access to raw req, res, next(). Identical to Express middleware. Use for request logging, CORS headers, body parsing, or tenant header extraction.

import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
  private readonly logger = new Logger('HTTP');

  use(req: Request, _res: Response, next: NextFunction): void {
    const start = Date.now();
    _res.on('finish', () => {
      this.logger.log(
        `${req.method} ${req.originalUrl} ${_res.statusCode} ${Date.now() - start}ms`,
      );
    });
    next();
  }
}

// Applied in the module's configure() method
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(RequestLoggerMiddleware).forRoutes('*');
  }
}

2.2 Guards

Determine whether a request is authorized to proceed. Return true/false (or throw). Guards have full access to the ExecutionContext, which means they can inspect metadata set by decorators.

import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ContainerService } from './container.service';

@Injectable()
export class ContainerOwnerGuard implements CanActivate {
  constructor(
    private readonly containerService: ContainerService,
    private readonly reflector: Reflector,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest();
    const containerId: string = req.params.id;
    const userId: string = req.user?.id;

    if (!userId) {
      throw new ForbiddenException('Authentication required');
    }

    const isOwner = await this.containerService.isOwner(containerId, userId);
    if (!isOwner) {
      throw new ForbiddenException(
        `User ${userId} does not own container ${containerId}`,
      );
    }

    return true;
  }
}

// Usage on a controller method
@UseGuards(JwtAuthGuard, ContainerOwnerGuard)
@Get(':id/vnc-token')
async getVncToken(@Param('id') id: string): Promise<VncTokenDto> {
  return this.containerService.issueVncToken(id);
}

Coming from Spring

Guards = Spring Security's @PreAuthorize annotations or AccessDecisionVoter. The Reflector utility reads custom metadata from decorators -- it is the NestJS equivalent of reading Spring @Secured or @RolesAllowed annotation values at runtime.

2.3 Interceptors

Wrap the route handler execution with before and after logic via RxJS Observable. This is the NestJS equivalent of Spring AOP @Around advice.

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Logger,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class TimingInterceptor implements NestInterceptor {
  private readonly logger = new Logger('Timing');

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now();
    const handler = `${context.getClass().name}.${context.getHandler().name}`;

    return next.handle().pipe(
      tap(() => {
        this.logger.log(`${handler} completed in ${Date.now() - start}ms`);
      }),
    );
  }
}

2.4 Pipes

Transform or validate input data before it reaches the handler. NestJS ships ValidationPipe (backed by class-validator) and ParseIntPipe, ParseUUIDPipe, etc.

import {
  PipeTransform,
  Injectable,
  BadRequestException,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';

@Injectable()
export class StrictValidationPipe implements PipeTransform {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) return value;

    const object = plainToInstance(metatype, value);
    const errors = await validate(object, {
      whitelist: true,          // strip unknown properties
      forbidNonWhitelisted: true, // throw on unknown properties
    });

    if (errors.length > 0) {
      const messages = errors.flatMap((e) =>
        Object.values(e.constraints ?? {}),
      );
      throw new BadRequestException(messages);
    }

    return object;
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

2.5 Exception Filters

Catch unhandled exceptions and transform them into structured HTTP responses. They run last in the pipeline.

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger('ExceptionFilter');

  catch(exception: unknown, host: ArgumentsHost): void {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const message =
      exception instanceof HttpException
        ? exception.message
        : 'Internal server error';

    this.logger.error(
      `${request.method} ${request.url} -> ${status}`,
      exception instanceof Error ? exception.stack : undefined,
    );

    response.status(status).json({
      statusCode: status,
      message,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

3. Custom Decorators

Decorators are NestJS's composition primitive. They replace Spring annotations and can combine metadata, validation, and extraction.

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { JwtPayload } from '../auth/jwt-payload.interface';

/**
 * Extracts the authenticated user (or a specific field) from the request.
 * Usage:
 *   @Get('profile') getProfile(@CurrentUser() user: JwtPayload)
 *   @Get('profile') getProfile(@CurrentUser('id') userId: string)
 */
export const CurrentUser = createParamDecorator(
  (data: keyof JwtPayload | undefined, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user: JwtPayload = request.user;
    return data ? user?.[data] : user;
  },
);

Compose multiple decorators into one using applyDecorators:

import { applyDecorators, UseGuards, SetMetadata } from '@nestjs/common';

export function Auth(...roles: string[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(JwtAuthGuard, RolesGuard),
  );
}

// Usage: @Auth('admin') instead of stacking three decorators

4. WebSocket Gateway -- VNC Relay

Critical for Warmwind's real-time VNC streaming between browser clients and Docker containers. The gateway manages Socket.io rooms, authenticates via JWT handshake, and proxies RFB frames bidirectionally.

graph LR
    Browser["Browser (noVNC)"] -->|Socket.io| GW["VncGateway"]
    GW -->|TCP RFB| Container["Docker Container :5900"]
    GW -->|Room: container:abc| Redis["Redis Adapter"]
    Redis -->|Fan-out| GW2["VncGateway (Node 2)"]
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
  ConnectedSocket,
  MessageBody,
  WsException,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger, UseGuards } from '@nestjs/common';
import { WsJwtGuard } from '../auth/ws-jwt.guard';
import { VncProxyService } from './vnc-proxy.service';
import { SessionService } from './session.service';

interface VncInputEvent {
  containerId: string;
  type: 'key' | 'pointer' | 'clipboard';
  data: Uint8Array;
}

@WebSocketGateway({
  namespace: '/vnc',
  cors: { origin: process.env.FRONTEND_ORIGIN ?? '*' },
  transports: ['websocket'],  // skip HTTP long-poll for latency
})
export class VncGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  private readonly logger = new Logger(VncGateway.name);

  constructor(
    private readonly vncProxy: VncProxyService,
    private readonly sessionService: SessionService,
  ) {}

  async handleConnection(client: Socket): Promise<void> {
    try {
      // Authenticate via JWT in handshake auth header
      const user = await this.sessionService.authenticateSocket(client);
      const containerId = client.handshake.query.containerId as string;

      if (!containerId) {
        throw new WsException('containerId query parameter required');
      }

      // Verify ownership before joining the room
      await this.sessionService.verifyOwnership(user.id, containerId);

      client.join(`container:${containerId}`);
      client.data = { userId: user.id, containerId };

      // Open upstream TCP connection to the container's VNC server
      await this.vncProxy.connect(client.id, containerId);

      this.logger.log(
        `Client ${client.id} connected to container ${containerId}`,
      );
    } catch (err) {
      this.logger.warn(`Connection rejected: ${err.message}`);
      client.disconnect(true);
    }
  }

  @SubscribeMessage('vnc:input')
  handleInput(
    @ConnectedSocket() client: Socket,
    @MessageBody() payload: VncInputEvent,
  ): void {
    // Forward keyboard/mouse/clipboard events to the container's VNC server
    this.vncProxy.forward(client.data.containerId, payload);
  }

  @SubscribeMessage('vnc:resize')
  async handleResize(
    @ConnectedSocket() client: Socket,
    @MessageBody() payload: { width: number; height: number },
  ): Promise<void> {
    await this.vncProxy.resize(client.data.containerId, payload);
  }

  async handleDisconnect(client: Socket): Promise<void> {
    const { containerId } = client.data ?? {};
    if (containerId) {
      await this.vncProxy.disconnect(client.id);
      this.logger.log(
        `Client ${client.id} disconnected from container ${containerId}`,
      );
    }
    await this.sessionService.cleanup(client.id);
  }
}

5. BullMQ Job Queue -- Container Lifecycle

Container creation, snapshotting, and teardown are long-running operations that must not block the HTTP response cycle. BullMQ provides Redis-backed, persistent, retryable job queues.

import { Injectable, Logger } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue, Job } from 'bullmq';

interface StartContainerDto {
  userId: string;
  image: string;
  resourceLimits: { cpus: number; memoryMb: number };
}

@Injectable()
export class ContainerService {
  private readonly logger = new Logger(ContainerService.name);

  constructor(
    @InjectQueue('containers') private readonly queue: Queue,
  ) {}

  async startContainer(dto: StartContainerDto): Promise<{ jobId: string }> {
    const job = await this.queue.add('start', dto, {
      attempts: 3,
      backoff: { type: 'exponential', delay: 2_000 },
      removeOnComplete: { age: 3600 },   // keep completed jobs for 1h
      removeOnFail: { age: 86_400 },     // keep failed jobs for 24h
      priority: dto.resourceLimits.cpus > 4 ? 10 : 1,  // lower = higher priority
    });

    this.logger.log(`Enqueued container start job ${job.id} for ${dto.image}`);
    return { jobId: job.id! };
  }

  async stopContainer(containerId: string): Promise<{ jobId: string }> {
    const job = await this.queue.add('stop', { containerId }, {
      attempts: 2,
      backoff: { type: 'fixed', delay: 1_000 },
    });
    return { jobId: job.id! };
  }
}
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';
import { Logger } from '@nestjs/common';
import { DockerService } from './docker.service';
import { EventEmitter2 } from '@nestjs/event-emitter';

@Processor('containers', {
  concurrency: 5,
  limiter: { max: 10, duration: 60_000 },  // max 10 jobs per minute
})
export class ContainerProcessor extends WorkerHost {
  private readonly logger = new Logger(ContainerProcessor.name);

  constructor(
    private readonly docker: DockerService,
    private readonly events: EventEmitter2,
  ) {
    super();
  }

  async process(job: Job): Promise<any> {
    switch (job.name) {
      case 'start':
        return this.handleStart(job);
      case 'stop':
        return this.handleStop(job);
      default:
        throw new Error(`Unknown job name: ${job.name}`);
    }
  }

  private async handleStart(job: Job<StartContainerDto>): Promise<string> {
    this.logger.log(`Starting container for image ${job.data.image}`);

    await job.updateProgress(10);
    const containerId = await this.docker.createContainer(job.data);

    await job.updateProgress(50);
    await this.docker.startContainer(containerId);

    await job.updateProgress(100);

    // Emit domain event for CQRS saga / subscription notification
    this.events.emit('container.started', {
      containerId,
      userId: job.data.userId,
      image: job.data.image,
    });

    return containerId;
  }

  private async handleStop(job: Job<{ containerId: string }>): Promise<void> {
    await this.docker.stopContainer(job.data.containerId);
    this.events.emit('container.stopped', {
      containerId: job.data.containerId,
    });
  }
}

6. CQRS Pattern

The @nestjs/cqrs module separates reads from writes via distinct command and query buses, with events propagating side effects through sagas and projections.

graph LR
    CMD["StartContainerCommand"] --> CB["CommandBus"]
    CB --> CH["StartContainerHandler"]
    CH --> EVT["ContainerStartedEvent"]
    EVT --> EB["EventBus"]
    EB --> SAGA["NotifyUserSaga"]
    EB --> PROJ["UpdateDashboardProjection"]
    QB["GetContainerQuery"] --> QBus["QueryBus"] --> QH["GetContainerHandler"] --> DB["Read Model (Postgres)"]
// Command
export class StartContainerCommand {
  constructor(
    public readonly userId: string,
    public readonly image: string,
    public readonly resourceLimits: { cpus: number; memoryMb: number },
  ) {}
}

// Command Handler
@CommandHandler(StartContainerCommand)
export class StartContainerHandler
  implements ICommandHandler<StartContainerCommand>
{
  constructor(private readonly containerService: ContainerService) {}

  async execute(command: StartContainerCommand): Promise<{ jobId: string }> {
    return this.containerService.startContainer({
      userId: command.userId,
      image: command.image,
      resourceLimits: command.resourceLimits,
    });
  }
}

// Domain Event
export class ContainerStartedEvent {
  constructor(
    public readonly containerId: string,
    public readonly userId: string,
  ) {}
}

// Saga: reacts to events and dispatches new commands
@Injectable()
export class ContainerSaga {
  @Saga()
  containerStarted = (events$: Observable<IEvent>): Observable<ICommand> => {
    return events$.pipe(
      ofType(ContainerStartedEvent),
      map((event) => new NotifyUserCommand(event.userId, 'Container ready')),
    );
  };
}

Coming from Spring

If you have used Axon Framework or Spring's ApplicationEventPublisher, the mental model is identical. CommandBus = Axon command bus. EventBus = ApplicationEventPublisher. @Saga() = Axon saga with the same "react to events, emit new commands" pattern. The key difference: NestJS CQRS events are in-memory by default -- for persistence you must add your own event store (e.g., EventStoreDB or a Postgres outbox table).


7. Testing Patterns

NestJS's DI container makes testing straightforward: override any provider with a mock at the module level.

7.1 Unit Testing a Service

import { Test, TestingModule } from '@nestjs/testing';
import { getQueueToken } from '@nestjs/bullmq';
import { ContainerService } from './container.service';

describe('ContainerService', () => {
  let service: ContainerService;
  let mockQueue: { add: jest.Mock };

  beforeEach(async () => {
    mockQueue = { add: jest.fn().mockResolvedValue({ id: 'job-123' }) };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ContainerService,
        { provide: getQueueToken('containers'), useValue: mockQueue },
      ],
    }).compile();

    service = module.get(ContainerService);
  });

  it('enqueues a start job with exponential backoff', async () => {
    const dto = {
      userId: 'u1',
      image: 'node:20',
      resourceLimits: { cpus: 2, memoryMb: 512 },
    };

    const result = await service.startContainer(dto);

    expect(result.jobId).toBe('job-123');
    expect(mockQueue.add).toHaveBeenCalledWith('start', dto, expect.objectContaining({
      attempts: 3,
      backoff: { type: 'exponential', delay: 2_000 },
    }));
  });
});

7.2 Integration Testing a Controller

import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('ContainerController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [AppModule],
    })
      .overrideProvider(DockerService)
      .useValue({
        createContainer: jest.fn().mockResolvedValue('ctr-abc'),
        startContainer: jest.fn().mockResolvedValue(undefined),
      })
      .compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  afterAll(() => app.close());

  it('POST /containers/start returns 201 with jobId', () => {
    return request(app.getHttpServer())
      .post('/containers/start')
      .set('Authorization', `Bearer ${testJwt}`)
      .send({ image: 'node:20', resourceLimits: { cpus: 1, memoryMb: 256 } })
      .expect(201)
      .expect((res) => {
        expect(res.body).toHaveProperty('jobId');
      });
  });
});

Coming from Spring

Test.createTestingModule() = @SpringBootTest with @MockBean. overrideProvider() = @MockBean replacement. The pattern is identical: boot a minimal application context, swap real implementations with mocks, and hit endpoints via supertest (= MockMvc/WebTestClient).


8. Bootstrap & Configuration

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe, Logger } from '@nestjs/common';
import { IoAdapter } from '@nestjs/platform-socket.io';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['error', 'warn', 'log', 'debug'],
  });

  // Global validation pipe -- all DTOs validated automatically
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  // Global exception filter
  app.useGlobalFilters(new GlobalExceptionFilter());

  // WebSocket adapter (swap for RedisIoAdapter in production)
  app.useWebSocketAdapter(new IoAdapter(app));

  // Required for onModuleDestroy hooks
  app.enableShutdownHooks();

  const port = process.env.PORT ?? 3000;
  await app.listen(port);
  Logger.log(`Application listening on port ${port}`, 'Bootstrap');
}
bootstrap();
What's new (2025--2026)

NestJS 11 ships Express v5 and Fastify v5 with breaking route syntax changes (path parameter patterns updated from :id regex to Express v5 syntax). New features include built-in structured JSON logging (replacing the console-based default logger), IntrinsicException for cleaner internal error handling, enhanced CQRS support with improved event-sourcing patterns, and first-class SWC compilation support for faster builds.


Glossary

Glossary

Module
Organizational unit decorated with @Module() that groups related controllers, services, and providers into a cohesive boundary. Equivalent to a Spring @Configuration class.
Provider
Any class or value registered in a module's providers array that the DI container can inject. Services, repositories, factories, and guards are all providers.
Dependency Injection (DI)
Pattern where the framework resolves and injects class dependencies automatically based on constructor type signatures and injection tokens.
Guard
Pipeline layer that determines whether a request should proceed, typically used for authentication and authorization. Returns boolean from canActivate().
Interceptor
Wraps route handler execution via RxJS Observable for cross-cutting concerns like logging, caching, response transformation, or timeout enforcement.
Pipe
Transforms or validates input data before it reaches the route handler. Built-in pipes include ValidationPipe, ParseIntPipe, ParseUUIDPipe.
Exception Filter
Catches thrown exceptions and maps them to structured HTTP/WS responses. Runs last in the pipeline.
Middleware
Express-compatible function running before guards. Has access to req, res, next() but not to ExecutionContext.
Gateway
WebSocket entry point decorated with @WebSocketGateway() for real-time bidirectional communication over Socket.io or native WebSocket.
CQRS
Command Query Responsibility Segregation -- separates read and write operations into distinct models connected by domain events.
Saga
CQRS component that listens to domain events and dispatches new commands, orchestrating multi-step business processes.
BullMQ
Redis-backed job queue library supporting delayed jobs, retries with backoff, rate limiting, concurrency control, and job prioritization. Successor to Bull.
Execution Context
NestJS abstraction (ExecutionContext) that unifies HTTP, WebSocket, and RPC contexts, allowing guards/interceptors to work across transport layers.
Decorator
TypeScript language feature (stage 3 proposal, finalized) used by NestJS to attach metadata to classes, methods, and parameters. Equivalent to Java/Scala annotations.