Bỏ qua để đến nội dung
Knowledge Hub

Provider scope trong NestJS: singleton, request và transient Trung cấp

Thời gian đọc ước tính: 6 phút

CheckoutService cần tenant của request hiện tại để chọn cấu hình thanh toán. Hai cực đều có giá:

  • Cách ngây thơ: middleware gán this.currentTenant vào service. Nest mặc định dùng một instance cho cả application, nên hai request chạy xen kẽ ghi đè field của nhau.
  • Cách ngược lại: gắn Scope.REQUEST lên mọi service liên quan, và Nest phải tạo cả cây object mới cho mỗi request.

Hai thái cực đều sai. Cần phân biệt dữ liệu theo request với vòng đời của cả service.

Demo count-demo.ts resolve CheckoutService trong 100 context khác nhau (giả lập 100 request, không có HTTP). Số lần constructor chạy, đo thật trên Nest 11:

Provider Scope Số instance sau 100 request Vì sao
Config DEFAULT 1 Dùng chung trong application context
Formatter TRANSIENT 2 Mỗi consumer inject (ReportService, InvoiceService) nhận một bản riêng; hai consumer này là default nên chỉ tạo một lần
ReportService, InvoiceService DEFAULT 1 mỗi loại Singleton, giữ Formatter của mình qua mọi request
RequestContext REQUEST 100 Một bản cho mỗi request context
CheckoutService DEFAULT (không khai báo) 100 Inject RequestContext nên bị kéo thành request-scoped

Hai dòng cuối là điểm hay bỏ sót: scope “bubble up” theo chuỗi inject, kể cả khi provider phía trên không khai báo gì. Nếu CheckoutService được inject vào controller, controller cũng trở thành request-scoped.

Đang tải diagram…

Khai báo scope:

@Injectable({ scope: Scope.REQUEST })
class RequestContext {}
@Injectable({ scope: Scope.TRANSIENT })
class Formatter {}

Với scoped provider trong standalone context, dùng app.resolve(token, contextId) thay cho app.get(). Trong ứng dụng HTTP, Nest tự tạo context ID cho từng request.

Đang tải diagram…

Cách Instance Chi phí Hợp với
Tham số Singleton Sửa chữ ký hàm Dữ liệu nghiệp vụ
ALS Singleton Dependency ngầm vào entry point Correlation ID, logger
Scope.REQUEST Mỗi request một cây Cấp phát theo request, lan scope State thật sự sống đúng một request (cache trong request, unit of work)

Tài liệu Nest nói request scope trong một ứng dụng thiết kế tốt không nên làm latency tăng quá khoảng 5%. Đó là con số của tài liệu, không phải cam kết cho hệ thống của bạn.

Đo với graph thật, đặc biệt khi một provider trung tâm như logger hay datasource bị kéo thành request-scoped, vì nó kéo theo mọi thứ phía trên.

Request scope cũng buộc code gắn với request của transport:

Transport Request scope
Cron Không có request (tài liệu Nest nêu Cron controller phải là singleton, không dùng request-scoped provider)
Microservice như Kafka Vẫn có context theo message, nhưng cách lấy request khác HTTP
WebSocket gateway Scope gắn với kết nối, không phải từng message

Kiểm tra transport trước khi dựa vào nó.

Khi request scope chỉ cần khác nhau theo tenant (vài chục tenant, hàng nghìn request), tạo lại cả cây cho mỗi request là phí. Durable provider cho phép Nest gom request theo một khoá (ví dụ tenant) và tái sử dụng cây dependency cho cả nhóm. Theo tài liệu Nest:

@Injectable({ scope: Scope.REQUEST, durable: true })
export class TenantConfig {}

Cần thêm một ContextIdStrategy quyết định request thuộc nhóm nào, đăng ký một lần ở bootstrap:

ContextIdFactory.apply(new AggregateByTenantContextIdStrategy());

Chiến lược đọc tenant từ request (ví dụ header x-tenant-id), trả payload chứa tenant ID để provider durable nhận qua @Inject(REQUEST). Hai điều kiện:

  • Số nhóm phải hữu hạn (cache theo tenant không giới hạn sẽ giữ vô hạn instance).
  • Đo heap với số tenant thật.

Repo này chưa chạy durable provider; code trên bám theo tài liệu Nest, hãy đọc mục “Durable providers” cho đúng phiên bản bạn dùng.

  1. Mặc định DEFAULT; chỉ đổi scope khi có state thật sự sống theo request.
  2. Không lưu currentUser hay currentTenant trong field của provider DEFAULT.
  3. Trước khi gắn Scope.REQUEST, liệt kê mọi provider phía trên sẽ bị kéo theo (đặc biệt logger, datasource).
  4. Metadata (requestId, tenant cho log) dùng ALS; dữ liệu nghiệp vụ truyền tham số.
  5. Test song song với nhiều tenant; một field mutable có thể pass test tuần tự nhưng race khi tải đồng thời.
  6. Đếm số instance tạo ra và đo p99 trước và sau khi đổi scope, dùng graph thật.
  7. Multi-tenant với số tenant hữu hạn thì cân nhắc durable provider và đo heap.

Tự kiểm tra: (1) Vì sao currentTenant trên service mặc định có thể sai khi chạy song song? (2) Transient Formatter inject vào consumer default có được tạo lại mỗi request không? (3) CheckoutService không khai báo scope nhưng inject provider REQUEST: nó có bao nhiêu instance sau 100 request?

Chạy demo:

Terminal window
npx --prefix examples ts-node examples/src/nestjs-provider-scopes/count-demo.ts
npx --prefix examples ts-node examples/src/nestjs-provider-scopes/scopes-demo.ts