NestJS Dependency Injection Scopes: Singleton, Request and Transient
Back in Module 1, you learned that NestJS providers default to singleton scope, meaning one shared instance serves the entire application. That's true for the vast majority of services you'll ever write, but NestJS actually supports two additional scopes, and understanding when (rarely) you need them, and the real performance cost of using them unnecessarily, is valuable, interview-relevant knowledge.
NestJS Dependency Injection Scopes: Learning Objectives
- Recall why singleton scope is NestJS's default and why it works for most providers.
- Understand request scope and when a provider genuinely needs per-request state.
- Understand transient scope and how it differs from request scope.
- Configure a provider's scope explicitly using the @Injectable() decorator's options.
- Recognize the performance trade-offs of request and transient scope versus the default.
NestJS Dependency Injection Scopes: Key Terms and Definitions
- Singleton scope: The default NestJS provider scope, where exactly one instance is created and shared across the entire application's lifetime.
- Request scope: A provider scope where a new instance is created for every single incoming request, then discarded once that request completes.
- Transient scope: A provider scope where a new instance is created every single time the provider is injected, even multiple times within the same request.
- Scope bubbling: The phenomenon where a provider depending on a request-scoped or transient provider also effectively becomes request-scoped or transient itself.
- Scope: { scope: Scope.REQUEST }: The configuration option passed to @Injectable() to change a provider's default scope.
How NestJS Dependency Injection Scopes Works: Detailed Explanation
By default, every NestJS provider is singleton-scoped: NestJS creates exactly one instance when the application starts, and every class that injects it receives that same shared instance for the entire lifetime of the application. This is efficient and correct for the overwhelming majority of services, which are typically stateless, meaning they don't store any request-specific data as instance properties, only performing operations based on whatever arguments are passed into their methods.
Request scope exists for the rare cases where a provider genuinely needs to hold state specific to a single incoming request, state that would be incorrect or even a security risk to share across different requests. A common example is a service that needs direct access to request-specific context, like a multi-tenant application's current tenant ID, stored as an instance property set once per request rather than passed as a parameter to every single method call. Configuring a provider as request-scoped, using @Injectable({ scope: Scope.REQUEST }), tells NestJS to create a brand-new instance of that provider for every incoming request, discarding it once the request completes, guaranteeing no state ever leaks between different users' concurrent requests.
Transient scope goes a step further: rather than one instance per request, a new instance is created every single time the provider is injected into anything, even multiple times within processing the same request. This is useful in narrower scenarios, such as a logging or utility service where each consumer needs its own independent instance, perhaps configured slightly differently, without any shared or request-level state at all.
The critical trade-off to understand deeply, and a common interview question, is performance and what's called scope bubbling. Creating a new instance for every request (or every injection) is inherently more expensive than reusing one shared singleton instance, since NestJS must re-run that provider's entire construction and dependency resolution process repeatedly rather than once. Additionally, if a singleton-scoped provider (like a widely-used core service) ends up depending on a request-scoped or transient provider somewhere in its dependency chain, that singleton provider itself effectively becomes request-scoped too, since NestJS cannot safely share a single instance of something that itself depends on per-request state. This bubbling effect can inadvertently make large portions of an application request-scoped, and therefore meaningfully slower, if request or transient scope is introduced carelessly deep in a commonly-used dependency chain.
Interview-Friendly Explanation
A strong interview or viva answer for nestjs dependency injection scopes must go beyond a one-line definition. Interviewers evaluating experienced backend developers reward answers that combine a precise definition, a clear working explanation, a real-world example, and a conclusion that highlights why it matters in production Node.js applications. Use the four-point framework below when answering under time pressure.
- Definition point: Singleton scope: The default NestJS provider scope, where exactly one instance is created and shared across the entire application's lifetime.
- Working point: Understand request scope and when a provider genuinely needs per-request state.
- Example point: Multi-tenant SaaS platforms are the most common real-world use case for request-scoped providers, exactly like TenantContextService, ensuring tenant identification never accidentally leaks between different customers' concurrent requests.
- Conclusion point: Singleton scope should remain your default assumption; only reach for request or transient scope with a specific, genuine reason.
How to Answer This in a Technical Interview
- Give a two-to-three sentence definition using correct NestJS and Node.js terminology.
- Add one specific example drawn from a real backend scenario such as an e-commerce, fintech, or SaaS API.
- Mention a relevant trade-off or alternative approach, since interviewers often probe this contrast.
- Close with one benefit, limitation, or production use case.
- Avoid vague answers that only restate the term — interviewers filter these out immediately.
Practical Scenario
Imagine you are scaling a production backend for a SaaS product, similar to systems used at companies like Razorpay, Freshworks, or Zomato. Every lesson in this module maps directly to a decision you will make while making that backend more advanced, performant, and maintainable at scale.
NestJS Dependency Injection Scopes: Architecture and Flow Diagram
Visualize the three scopes and their instance creation frequency:
[Singleton (default)] --> One instance created at startup --> Shared by every consumer, forever
[Request] --> New instance created per incoming HTTP request --> Shared within that one request, then discarded
[Transient] --> New instance created every single time it's injected --> Never shared, even within the same request
Scope bubbling: [SingletonService depends on RequestScopedService] --> SingletonService effectively becomes request-scoped too
NestJS Dependency Injection Scopes: Scope Reference Table
| Scope | Instance Lifetime | Performance Cost | Typical Use Case |
|---|---|---|---|
| Singleton (default) | Entire application lifetime | Lowest — created once | The vast majority of stateless services |
| Request | One incoming HTTP request | Higher — recreated per request | Genuinely needing per-request state (e.g. tenant context) |
| Transient | Every single injection | Highest — recreated per injection | Independent, unshared utility instances |
NestJS Dependency Injection Scopes: NestJS Code Example
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
// Request-scoped: a new instance per incoming request, with access to that request
@Injectable({ scope: Scope.REQUEST })
export class TenantContextService {
private tenantId: string;
constructor(@Inject(REQUEST) private readonly request: Request) {
// Runs fresh for every request; safe to read request-specific headers here
this.tenantId = (request.headers['x-tenant-id'] as string) || 'default';
}
getTenantId(): string {
return this.tenantId;
}
}
// Transient: a brand-new instance every time it's injected, anywhere
@Injectable({ scope: Scope.TRANSIENT })
export class RequestIdGenerator {
private readonly id = Math.random().toString(36).slice(2, 10);
getId(): string {
return this.id; // each injected instance has its own independent, unique id
}
}
// A default, singleton-scoped service (the overwhelming majority case)
@Injectable() // no scope specified = Scope.DEFAULT (singleton)
export class ProductsService {
private cache = new Map(); // safe to share across every request, since it's not user-specific
findAll() {
return Array.from(this.cache.values());
}
}
TenantContextService is explicitly marked request-scoped, allowing it to safely inject Express's raw Request object using NestJS's special REQUEST token, and read a request-specific header to determine the current tenant, state that would be dangerously incorrect to share across different users' concurrent requests if this were singleton-scoped instead. RequestIdGenerator demonstrates transient scope: every single place that injects it receives a completely separate instance with its own independently generated id, useful when even sharing within the same request would be inappropriate. ProductsService, with no scope specified at all, defaults to singleton, which is entirely appropriate here since its in-memory cache is meant to be shared globally across all requests, not scoped to any individual one.
Real-World NestJS Dependency Injection Scopes Industry Examples
- Multi-tenant SaaS platforms are the most common real-world use case for request-scoped providers, exactly like TenantContextService, ensuring tenant identification never accidentally leaks between different customers' concurrent requests.
- Request-tracing and correlation ID systems sometimes use request-scoped providers to attach a unique trace ID to every log statement generated during the processing of a single request, aiding debugging in distributed systems.
- The vast majority of production NestJS services, database repositories, business logic services, utility helpers, remain singleton-scoped by default, since introducing unnecessary request or transient scope would degrade performance without any real benefit.
- Performance-conscious teams specifically audit their dependency graphs to catch accidental scope bubbling, where a deeply shared, otherwise-singleton service unintentionally becomes request-scoped due to one small, careless dependency.
NestJS Dependency Injection Scopes Interview Questions and Answers
Q1. What is the default provider scope in NestJS, and why is it appropriate for most services?
Short answer: The default scope is singleton, meaning exactly one instance of a provider is created for the entire application's lifetime and shared by every consumer. This is appropriate for most services because they are typically stateless, performing operations based on method arguments rather than storing request-specific data as instance properties.
Detailed explanation: By default, every NestJS provider is singleton-scoped: NestJS creates exactly one instance when the application starts, and every class that injects it receives that same shared instance for the entire lifetime of the application. This is efficient and correct for the overwhelming majority of services, which are typically stateless, meaning they don't store any request-specific data as instance properties, only performing operations based on whatever arguments are passed into their methods. Request scope exists for the rare cases where a provider genuinely needs to hold state specific to a single incoming request, state that would be incorrect or even a security risk to share across different requests. A common example is a service that needs direct access to request-specific context, like a multi-tenant application's current tenant ID, stored as an instance property set once per request rather than passed as a parameter to every single method call. Configuring a provider as request-scoped, using @Injectable({ scope: Scope.REQUEST }), tells NestJS to create a brand-new instance of that provider for every incoming request, discarding it once the request completes, guaranteeing no state ever leaks between different users' concurrent requests. Transient scope goes a step further: rather than one instance per request, a new instance is created every single time the provider is injected into anything, even multiple times within processing the same request. This is useful in narrower scenarios, such as a logging or utility service where each consumer needs its own independent instance, perhaps configured slightly differently, without any shared or request-level state at all. The critical trade-off to understand deeply, and a common interview question, is performance and what's called scope bubbling. Creating a new instance for every request (or every injection) is inherently more expensive than reusing one shared singleton instance, since NestJS must re-run that provider's entire construction and dependency resolution process repeatedly rather than once. Additionally, if a singleton-scoped provider (like a widely-used core service) ends up depending on a request-scoped or transient provider somewhere in its dependency chain, that singleton provider itself effectively becomes request-scoped too, since NestJS cannot safely share a single instance of something that itself depends on per-request state. This bubbling effect can inadvertently make large portions of an application request-scoped, and therefore meaningfully slower, if request or transient scope is introduced carelessly deep in a commonly-used dependency chain.
Practical example: Multi-tenant SaaS platforms are the most common real-world use case for request-scoped providers, exactly like TenantContextService, ensuring tenant identification never accidentally leaks between different customers' concurrent requests.
Interview tip: Singleton (default): one instance, entire app lifetime — appropriate for the vast majority of stateless services.
Revision hook: Singleton scope should remain your default assumption; only reach for request or transient scope with a specific, genuine reason.
Q2. When would you use a request-scoped provider instead of the default singleton scope?
Short answer: Request scope is appropriate when a provider genuinely needs to hold state specific to a single incoming request, such as a multi-tenant application's current tenant ID, where sharing that state across different concurrent requests via a singleton instance would be incorrect or a security risk.
Detailed explanation: TenantContextService is explicitly marked request-scoped, allowing it to safely inject Express's raw Request object using NestJS's special REQUEST token, and read a request-specific header to determine the current tenant, state that would be dangerously incorrect to share across different users' concurrent requests if this were singleton-scoped instead. RequestIdGenerator demonstrates transient scope: every single place that injects it receives a completely separate instance with its own independently generated id, useful when even sharing within the same request would be inappropriate. ProductsService, with no scope specified at all, defaults to singleton, which is entirely appropriate here since its in-memory cache is meant to be shared globally across all requests, not scoped to any individual one.
Practical example: Request-tracing and correlation ID systems sometimes use request-scoped providers to attach a unique trace ID to every log statement generated during the processing of a single request, aiding debugging in distributed systems.
Interview tip: Request: one instance per incoming request — needed only when genuinely holding per-request state.
Revision hook: Multi-tenancy and per-request context are the most common legitimate reasons to use request scope in real applications.
Q3. What is scope bubbling, and why is it an important performance consideration?
Short answer: Scope bubbling occurs when a singleton-scoped provider depends, directly or indirectly, on a request-scoped or transient provider, causing that singleton provider to effectively become request-scoped itself, since NestJS cannot safely share one instance of something depending on per-request state. This can unintentionally make large portions of an application request-scoped and therefore slower if introduced carelessly.
Detailed explanation: NestJS provides three provider scopes: singleton, the default, where one instance is shared across the entire application's lifetime and is appropriate for the vast majority of stateless services; request scope, where a new instance is created for each incoming HTTP request, useful when a provider genuinely needs to hold per-request state like a multi-tenant application's current tenant ID; and transient scope, where an entirely new instance is created for every single injection point, even within the same request. Configuring scope explicitly through @Injectable({ scope: Scope.REQUEST }) or Scope.TRANSIENT comes with a real performance cost compared to the default singleton, compounded by scope bubbling, where a singleton depending on a request-scoped or transient provider effectively inherits that broader scope too. Understanding these trade-offs ensures scope is used deliberately, only where genuinely needed, rather than by default.
Practical example: The vast majority of production NestJS services, database repositories, business logic services, utility helpers, remain singleton-scoped by default, since introducing unnecessary request or transient scope would degrade performance without any real benefit.
Interview tip: Transient: one instance per injection point, even within the same request — narrowest, most specialized use case.
Revision hook: Scope bubbling is a subtle but real performance trap worth actively auditing for in larger codebases.
Q4. What is the difference between request scope and transient scope?
Short answer: Request scope creates one new instance per incoming HTTP request, shared by every consumer within that same request. Transient scope goes further, creating a completely new instance every single time the provider is injected, even multiple times within the processing of the same request, meaning instances are never shared at all.
Detailed explanation: By default, every NestJS provider is singleton-scoped: NestJS creates exactly one instance when the application starts, and every class that injects it receives that same shared instance for the entire lifetime of the application. This is efficient and correct for the overwhelming majority of services, which are typically stateless, meaning they don't store any request-specific data as instance properties, only performing operations based on whatever arguments are passed into their methods. Request scope exists for the rare cases where a provider genuinely needs to hold state specific to a single incoming request, state that would be incorrect or even a security risk to share across different requests. A common example is a service that needs direct access to request-specific context, like a multi-tenant application's current tenant ID, stored as an instance property set once per request rather than passed as a parameter to every single method call. Configuring a provider as request-scoped, using @Injectable({ scope: Scope.REQUEST }), tells NestJS to create a brand-new instance of that provider for every incoming request, discarding it once the request completes, guaranteeing no state ever leaks between different users' concurrent requests. Transient scope goes a step further: rather than one instance per request, a new instance is created every single time the provider is injected into anything, even multiple times within processing the same request. This is useful in narrower scenarios, such as a logging or utility service where each consumer needs its own independent instance, perhaps configured slightly differently, without any shared or request-level state at all. The critical trade-off to understand deeply, and a common interview question, is performance and what's called scope bubbling. Creating a new instance for every request (or every injection) is inherently more expensive than reusing one shared singleton instance, since NestJS must re-run that provider's entire construction and dependency resolution process repeatedly rather than once. Additionally, if a singleton-scoped provider (like a widely-used core service) ends up depending on a request-scoped or transient provider somewhere in its dependency chain, that singleton provider itself effectively becomes request-scoped too, since NestJS cannot safely share a single instance of something that itself depends on per-request state. This bubbling effect can inadvertently make large portions of an application request-scoped, and therefore meaningfully slower, if request or transient scope is introduced carelessly deep in a commonly-used dependency chain.
Practical example: Performance-conscious teams specifically audit their dependency graphs to catch accidental scope bubbling, where a deeply shared, otherwise-singleton service unintentionally becomes request-scoped due to one small, careless dependency.
Interview tip: Scope bubbling: a singleton depending on a request/transient provider effectively inherits that broader scope too.
Revision hook: Understanding these trade-offs, not just the syntax, is what separates a surface-level answer from a strong one in interviews.
NestJS Dependency Injection Scopes MCQs and Practice Questions
1. What is the default scope for a NestJS provider if none is explicitly specified?
- Scope.REQUEST
- Scope.TRANSIENT
- Scope.DEFAULT (singleton)
- There is no default; scope must always be specified
Answer: C. Scope.DEFAULT (singleton)
Explanation: Unless explicitly configured otherwise, every NestJS provider defaults to singleton scope, meaning one shared instance serves the entire application.
Concept link: Singleton (default): one instance, entire app lifetime — appropriate for the vast majority of stateless services.
Why this matters: Singleton scope should remain your default assumption; only reach for request or transient scope with a specific, genuine reason.
2. Which scope creates a new provider instance for every single incoming HTTP request?
- Singleton
- Request
- Transient
- Global
Answer: B. Request
Explanation: Request scope creates a fresh instance of the provider for each incoming request, which is then shared by any consumers within that same request before being discarded.
Concept link: Request: one instance per incoming request — needed only when genuinely holding per-request state.
Why this matters: Multi-tenancy and per-request context are the most common legitimate reasons to use request scope in real applications.
3. What happens if a singleton-scoped service depends on a request-scoped service?
- Nothing changes; it remains singleton
- The singleton-scoped service effectively also becomes request-scoped, a phenomenon called scope bubbling
- NestJS throws a compile-time error
- The request-scoped dependency is silently ignored
Answer: B. The singleton-scoped service effectively also becomes request-scoped, a phenomenon called scope bubbling
Explanation: Since a singleton cannot safely hold onto request-specific state shared across all requests, depending on a request-scoped provider causes the dependent provider to effectively bubble up to request scope as well.
Concept link: Transient: one instance per injection point, even within the same request — narrowest, most specialized use case.
Why this matters: Scope bubbling is a subtle but real performance trap worth actively auditing for in larger codebases.
4. Which scope creates a brand-new instance every single time a provider is injected, even multiple times in one request?
- Singleton
- Request
- Transient
- Static
Answer: C. Transient
Explanation: Transient scope goes further than request scope, ensuring a completely new, independent instance is created for every individual injection point, never shared even within the same request.
Concept link: Scope bubbling: a singleton depending on a request/transient provider effectively inherits that broader scope too.
Why this matters: Understanding these trade-offs, not just the syntax, is what separates a surface-level answer from a strong one in interviews.
Common NestJS Dependency Injection Scopes Mistakes to Avoid
- Using request or transient scope by default 'just in case', without a genuine need, unnecessarily degrading application performance.
- Not realizing that a widely-depended-upon singleton service has become request-scoped due to scope bubbling from one careless dependency deep in its chain.
- Storing request-specific state as instance properties on a singleton-scoped service, causing data to incorrectly leak or overwrite between different users' concurrent requests.
- Forgetting that request-scoped providers require re-instantiation and re-injection on every request, which is measurably more expensive than a singleton at high request volumes.
NestJS Dependency Injection Scopes: Interview Notes and Exam Tips
- Singleton (default): one instance, entire app lifetime — appropriate for the vast majority of stateless services.
- Request: one instance per incoming request — needed only when genuinely holding per-request state.
- Transient: one instance per injection point, even within the same request — narrowest, most specialized use case.
- Scope bubbling: a singleton depending on a request/transient provider effectively inherits that broader scope too.
Key NestJS Dependency Injection Scopes Takeaways
- Singleton scope should remain your default assumption; only reach for request or transient scope with a specific, genuine reason.
- Multi-tenancy and per-request context are the most common legitimate reasons to use request scope in real applications.
- Scope bubbling is a subtle but real performance trap worth actively auditing for in larger codebases.
- Understanding these trade-offs, not just the syntax, is what separates a surface-level answer from a strong one in interviews.
NestJS Dependency Injection Scopes: Summary
NestJS provides three provider scopes: singleton, the default, where one instance is shared across the entire application's lifetime and is appropriate for the vast majority of stateless services; request scope, where a new instance is created for each incoming HTTP request, useful when a provider genuinely needs to hold per-request state like a multi-tenant application's current tenant ID; and transient scope, where an entirely new instance is created for every single injection point, even within the same request. Configuring scope explicitly through @Injectable({ scope: Scope.REQUEST }) or Scope.TRANSIENT comes with a real performance cost compared to the default singleton, compounded by scope bubbling, where a singleton depending on a request-scoped or transient provider effectively inherits that broader scope too. Understanding these trade-offs ensures scope is used deliberately, only where genuinely needed, rather than by default.