Leadline Architecture Design

Dependency injection

Tokens and metadata, request scope and DI sub-trees, durable contexts, and multi-tenant data isolation in NestJS.

Implicit and explicit injection

NestJS resolves constructor dependencies in two ways:

  1. Implicit: parameter types are read from TypeScript metadata (reflect-metadata). Works for classes when emitDecoratorMetadata is enabled and the provider is registered by class token.
  2. Explicit: @Inject(token) names the exact token—required for interfaces (they erase at compile time), string or symbol tokens, useValue / useFactory providers, and many dynamic registrations.

Prefer implicit injection for class dependencies to reduce boilerplate and improve refactor safety. Use symbols (or well-namespaced string constants) for custom tokens to avoid collisions and typos.

export const COFFEES_DATA_SOURCE = Symbol("COFFEES_DATA_SOURCE");

@Injectable()
export class CoffeesService {
  constructor(@Inject(COFFEES_DATA_SOURCE) private readonly dataSource: CoffeesDataSource) {}
}
@Module({
  providers: [
    CoffeesService,
    { provide: COFFEES_DATA_SOURCE, useValue: /* concrete adapter */ [] },
  ],
})
export class CoffeesModule {}

Alternatively, use a concrete class as the token when the abstraction does not need to remain an interface.

Application graph and DI sub-trees

The application graph is the static module and provider graph built at bootstrap. A DI sub-tree is an isolated resolution context keyed by a context id—for example, one tree per HTTP request for request-scoped providers.

  • Request-scoped providers are created when a triggering scope exists (such as an HTTP request).
  • Scope bubbles: if A depends on request-scoped B, then A becomes request-scoped as well.
  • ModuleRef.resolve() can create or reuse sub-trees; associating the same context id with the same request yields the same instances for that tree.
import { Injectable, Scope } from "@nestjs/common";
import { REQUEST } from "@nestjs/core";
import type { Request } from "express";

@Injectable({ scope: Scope.REQUEST })
export class TagsService {
  constructor(@Inject(REQUEST) private readonly request: Request) {}
}

Context id lifecycle: context ids are ordinary objects; retaining them in long-lived collections prevents garbage collection of the associated sub-tree and can cause memory leaks. Prefer letting scopes end with the request unless you deliberately extend lifetime.

Propagating request context to asynchronous handlers

Event subscribers are not request-scoped. When a handler must read request-scoped services, create a context id early, register the request with registerRequestByContextId, pass the context id on the event payload, and resolve request-scoped providers inside the handler with moduleRef.resolve(Service, contextId). Avoid storing large payloads on events; keep identifiers and context handles minimal.

Durable providers and shared sub-trees

Request-scoped graphs recreated on every request can be expensive at high throughput. Durable behavior (via stable context id factories) groups requests that share stable characteristics—tenant id, locale, API version—and reuses DI sub-trees for that group when varying per-request data does not affect provider wiring.

Typical flow:

  1. Derive a stable context key from headers, JWT claims, or subdomain.
  2. Create or reuse a context id for that key.
  3. Resolve services through that context; register the current request when the framework requires it.

Plan eviction for long-running processes so context maps do not grow without bound.

Multi-tenancy strategies

Multi-tenancy serves many customers from one deployment while isolating data and configuration.

Silo (database per tenant)

Application Tenant 1 DB Tenant 2 DB Tenant 3 DB

Strong isolation and straightforward per-tenant operations; higher cost and more complex onboarding and cross-tenant reporting.

Pool (shared database and schema)

Application Shared database Shared tables with tenant id

Efficient and simple to operate; requires strict query discipline and auditing to prevent cross-tenant leakage.

Bridge (shared database, schema per tenant)

Application Shared database Schema tenant_a Schema tenant_b

Balances isolation and efficiency; connection and migration tooling must understand schema routing.

Tenant identification in Nest

Common sources: subdomain, X-Tenant-ID header, JWT claim, API key lookup, or path prefix. Centralize resolution (for example in middleware) and reuse the result in logging, persistence, and durable context factories.

This page and links

Loading map…

On this page