Skip to content

ADR-0001. Các mức cô lập multi-tenancy (Pool / Bridge / Silo)

TrườngGiá trị
Trạng tháiDraft (Đề xuất)
Ngày2026-05-22
Người quyết địnhPhat Nguyen, Architecture
Phạm viCross-cutting — datasource @nx/core, mọi service, deployment
Thay thế
Bối cảnh sản phẩmChiến lược Multi-Tenancy (PRD)

Bối cảnh

Bài toán

  • BANA chạy một database dùng chung (nx_seller) với một connection pool tĩnh mỗi service (PostgresCoreDataSource gọi new Pool() một lần). Tenant chỉ được phân biệt bằng lọc merchantId / organizerId trong query repository — tức mô hình Pool.
  • Ta cần hỗ trợ mức cô lập mạnh hơn (DB riêng, stack riêng) cho một số Org trong khi giữ Pool rẻ cho số đông, có thể di chuyển Org giữa các mô hình — gồm cả chiều khó: gộp Silo → Pool.

Điều thúc đẩy (Trigger)

  • Quy hoạch deployment & vận hành dài hạn. Số tenant và độ đa dạng hợp đồng đang tăng; chốt cứng một mô hình toàn cục bây giờ sẽ rất đắt để gỡ về sau.

Hiện trạng (AS-IS)

Khía cạnhHiện tại
Cô lập DBMột nx_seller chung, một pool tĩnh/service
Cột tenantmerchantId (chính), organizerId (cha) trên ~52 bảng
Cách thực thi cô lậpLọc ở tầng app; không RLS, không schema-per-tenant
Phân giải tenantJWT claim (organizers[], merchants[]) → lọc theo request context
RoutingTheo token; không theo subdomain/host
IDSnowflake (toàn cục duy nhất) qua IdGenerator
DeploymentK8s trên VNPAY Cloud (Kustomize, cluster staging/prod tách biệt); gateway Traefik

Quyết định

Áp dụng mô hình hybrid, phân mức (tiered) trong đó mức cô lập là thuộc tính theo từng Org, quyết định lúc chạy — không phải hằng số cho cả hệ thống. Đơn vị cô lập là Organizer.

MứcServiceDatabaseMặc định cho
POOLChungnx_seller chung, lọc organizerIdMọi Org (mặc định)
BRIDGEChungMột DB mỗi OrgOrg cần cô lập dữ liệu
SILOStack riêngMột DB mỗi OrgEnterprise / on-prem

Ba cơ chế nền tảng giúp điều này khả thi — tất cả nằm trong @nx/core, không đụng code nghiệp vụ:

  1. Tenant Registry — bảng orgId → { isolationTier, datasourceRef }. Mọi Org mặc định POOL.
  2. Connection Resolver — nâng PostgresCoreDataSource từ một pool tĩnh thành bộ phân giải pool theo tenant, đọc registry và cache kết nối. Đây là thay đổi chặn duy nhất; mọi thứ khác xây trên nó.
  3. Bộ cấp phát Snowflake WorkerId — cấp worker/node ID tập trung để các silo chạy độc lập không bao giờ sinh ID trùng (formalize convention APP_ENV_NODE_ID sẵn có, xem payment ADR-0002).

Tính có hướng của migration

ChiềuĐộ khóCơ chế
POOL → SILO (tách)DễCopy filtered theo organizerId (logical replication / pg_dump --where), cutover, flip registry
SILO → POOL (gộp)Khó nhưng khả thiImport giữ nguyên Snowflake ID, verify không orphan FK, flip registry, khóa silo cũ

Vì sao gộp an toàn ở đây: Snowflake ID toàn cục duy nhất nên import row của silo vào bảng chung không trùng PK — đúng đặc tính khiến merge dựa trên auto-increment gần như bất khả. Voucher sequence theo merchant được scope bởi merchantId nên số chứng từ human-readable cũng không trùng chéo giữa các Org.

Hệ quả

LợiHại
Cô lập thành một "núm xoay" theo Org, không phải viết lạiConnection Resolver thêm phức tạp vào hot path tầng data
Org mới onboard tức thì (POOL mặc định)Cần cache kết nối theo tenant (vòng đời, eviction)
Snowflake ID giúp gộp Silo→Pool khả thiWorkerId phải quản trị tập trung nếu không merge sẽ vỡ
Code nghiệp vụ/repository không đổi giữa các mứcMigrate schema phải fan-out qua N database (Bridge/Silo)
Khớp topology K8s/Kustomize + Traefik sẵn cóProvision Bridge/Silo cần tự động hóa trước khi scale

Phương án đã cân nhắc

Phương ánLợiHạiVì sao loại
Chỉ PoolVận hành đơn giản nhấtKhông cô lập được, không lên on-premMất khách enterprise/compliance
Chỉ SiloCô lập tối đaChi phí cao nhất, onboard chậm, merge đắtSai cho POS SMB đại trà
Chốt một mức toàn cục ngayQuyết định đơn giảnLock-in; trả "thuế migration" sau dưới áp lựcQuá sớm; nhu cầu còn đang thăm dò
Routing tenant theo subdomain/hostPattern SaaS phổ biếnPhải làm lại URL client + mô hình tokenToken-based đã chạy; không cần
Postgres RLS thay lọc appCô lập do DB thực thiMigrate lớn mọi đường queryNgoài phạm vi quyết định này; ghi nhận là câu hỏi mở

Câu hỏi mở

  • Thị trường mục tiêu (SMB vs enterprise) — nghiêng hybrid, còn thăm dò.
  • Ràng buộc compliance / data-residency / on-prem — chưa xác định; nếu có sẽ khiến SILO + Helm chart portable thành bắt buộc.
  • BRIDGE là mức cố định hay chỉ trung chuyển sang SILO?
  • Dùng RLS hay giữ lọc tầng app cho POOL?

Hoàn thành khi

  • [ ] Tenant Registry tồn tại; mọi Org có isolationTier (mặc định POOL).
  • [ ] PostgresCoreDataSource phân giải kết nối theo tenant context từ registry.
  • [ ] Snowflake worker ID được cấp phát tập trung (không hai node trùng một).
  • [ ] Có runbook cho split (Pool→Silo) và merge (Silo→Pool).
  • [ ] Nâng trạng thái Draft → Accepted khi đã trả lời câu hỏi thị trường & compliance.

Tham chiếu

  • Chiến lược Multi-Tenancy (PRD)
  • packages/core/src/datasources/postgres-core.datasource.ts — pool tĩnh cần nâng cấp
  • packages/core/src/utilities/request.utility.ts — trích xuất tenant-context hiện tại
  • Payment ADR-0002 — tiền lệ phân vùng Snowflake NODE_ID
  • AWS SaaS Lens — pattern cô lập Pool / Bridge / Silo

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.