title: 'Injection scopes · Nael Framework' description: 'Understand singleton, request, and transient lifecycles in the Nael IoC container.'

Fundamentals

Injection scopes

Providers can live for the entire application, per incoming request, or be created every time they are injected. Choosing the right scope keeps expensive dependencies cached while still giving you access to per-request metadata when you need it.

Singleton (default)

Without extra options, every @Injectable() is a singleton. It is instantiated once during application bootstrap and shared with all consumers. Use this for stateless services, caches, config readers, or clients that should be reused.

Default singleton provider
import { Injectable } from '@nl-framework/core';@Injectable()export class FeatureFlagService {  private cache = new Map<string, boolean>();  isEnabled(flag: string) {    return this.cache.get(flag) ?? false;  }}

Be careful not to store request-specific data in singletons. Prefer method arguments or scoped providers for that data.

Request scope

Set scope: Scope.REQUEST to create a new instance per HTTP or GraphQL request. The container ties the lifecycle to the current execution context so you can access headers, user info, or trace IDs without manual plumbing.

Request-scoped logger
import { Injectable, Scope } from '@nl-framework/core';import { RequestContext } from '@nl-framework/http';@Injectable({ scope: Scope.REQUEST })export class RequestLogger {  constructor(private readonly ctx: RequestContext) {}  log(message: string) {    console.log('[' + this.ctx.request.url + ']', message);  }}

Request-scoped services can inject other request-scoped or singleton providers. Avoid injecting them into singletons unless you also mark the consuming provider as request-scoped, otherwise Nael will throw a circular scope error.

Transient scope

Transient providers are created every time they are injected. They never share state between injections, making them ideal for lightweight value objects, builders, or utilities that should produce a fresh instance per use.

Transient factory
import { Injectable, Scope, Inject } from '@nl-framework/core';import { randomUUID } from 'node:crypto';@Injectable({ scope: Scope.TRANSIENT })export class JobIdFactory {  readonly id = randomUUID();}@Injectable()export class JobsService {  constructor(@Inject(JobIdFactory) private readonly jobIdFactory: JobIdFactory) {}  run() {    return this.jobIdFactory.id;  }}

Because transient providers can fan out quickly, keep them cheap to create. They can inject singletons freely.

Scopes outside HTTP

Request scope also applies to GraphQL resolvers, scheduled jobs, and any custom context you establish via the core container. For advanced scenarios, you can create your own context IDs to isolate providers per unit of work.

GraphQL request scope
import { Injectable, Scope } from '@nl-framework/core';import { GraphqlExecutionContext } from '@nl-framework/graphql';@Injectable({ scope: Scope.REQUEST })export class TenantContext {  private tenantId?: string;  setFromExecution(ctx: GraphqlExecutionContext) {    this.tenantId = ctx.getContext().tenantId;  }  getId() {    return this.tenantId;  }}
Manually managed context
const app = await createHttpApplication(AppModule);const appContext = app.getApplicationContext();const contextId = appContext.createContextId('job-42');try {  const tenant = await appContext.get(TenantContext, { contextId });  tenant.setFromExecution(jobContext);  await jobRunner.execute(tenant);} finally {  appContext.releaseContext(contextId);}

To populate data, hook into the relevant execution context (HTTP guards, GraphQL interceptors, schedulers, etc.) and set values on the scoped service.