---
title: "Thiết kế nền tảng multi-tenant - control plane, tenant isolation và quota"
description: "Khóa học về thiết kế app SaaS phục vụ nhiều doanh nghiệp trên cùng một hệ thống, xoay quanh câu hỏi: dữ liệu và tài nguyên này là của tenant nào. Bạn sẽ chọn được cách lưu dữ liệu, chặn dữ liệu lọt sang tenant khác bằng RLS, đặt quota, ghi usage đúng để tính tiền và quản lý tenant bằng control plane."
canonical: "https://200lab.io/courses/thiet-ke-nen-tang-multi-tenant-control-plane-tenant-isolation-va-quota"
type: "course"
course_type: "course"
learning_type: "free_form"
published: "2026-10-13T08:03:31Z"
students: 0
---

## Khóa học này mang lại gì cho bạn

App SaaS bạn xây cùng agent phục vụ nhiều doanh nghiệp trên một Postgres. Mỗi doanh nghiệp là một tenant: khách hàng đó cùng toàn bộ dữ liệu, người dùng và cấu hình của họ trong app. App kiểu này gọi là multi-tenant, tức một app và một bộ hạ tầng phục vụ nhiều tenant cùng lúc.

Khi app có vài trăm tenant, một khách thấy danh bạ của công ty khác trong file vừa xuất, một khách khác nhập file lớn làm mọi khách chậm theo, và agent đề xuất cho mỗi khách một database riêng. Bạn cần biết nên sửa ở đâu, và cần hỏi agent những số liệu nào trước khi làm theo đề xuất đó.

- **Chọn cách lưu dữ liệu của tenant** — Bạn chọn giữa dùng chung database (shared), cho mỗi tenant một database riêng (dedicated) và kết hợp cả hai (hybrid) theo các constraint đo được, như tenant lớn làm chậm tenant khác tới đâu hay khôi phục riêng một tenant mất bao lâu. Bạn cũng nói được database riêng chặn được loại lỗi nào và không chặn được loại lỗi nào.
- **Chặn dữ liệu lọt sang tenant khác bằng RLS** — Bạn giải thích được vì sao code đã lọc theo `tenant_id` vẫn để lọt dữ liệu qua những câu SQL viết tay không đi qua lớp lọc của app. RLS (Row Level Security) là cơ chế để Postgres tự lọc dòng theo tenant đang gọi và từ chối trả về dòng của tenant khác. Bạn kiểm tra được những điều kiện về bảng, role kết nối và transaction mà RLS cần để thật sự chặn được dữ liệu của tenant khác.
- **Đưa gói dịch vụ và giới hạn vào bảng** — Bạn đưa gói dịch vụ, entitlement (từng quyền hay giới hạn cụ thể mà gói cho tenant) và giới hạn riêng của từng tenant vào bảng, thay vì để trong các câu `if` rải rác khắp code. Bạn đặt quota, tức phần tài nguyên tối đa một tenant được dùng trong một kỳ, bằng đơn vị bám sát chi phí thật.
- **Ghi usage đủ để tính tiền** — Usage là lượng tài nguyên có tính tiền mà mỗi tenant đã dùng, ví dụ số token. Token là đơn vị mà provider LLM, tức service bên ngoài chạy model AI cho trợ lý của app, dùng để đếm chữ và tính tiền. Bạn tìm ra những lời gọi provider đã tính tiền mà app không ghi usage, và giữ nhịp tiêu ngân sách để một tenant không vượt ngân sách tháng quá xa khi provider báo usage về trễ.
- **Chia worker và hạn mức của provider giữa các tenant** — Worker là tiến trình chạy việc nền. Bạn thêm giới hạn theo tenant vào cách worker lấy job, để một tenant chạy việc lớn không bắt mọi tenant khác chờ hàng giờ. Khi tenant nhỏ vẫn chậm, bạn lần ngược từ triệu chứng về tài nguyên dùng chung đang chạm trần, như disk của database hay hạn mức token của provider.
- **Quản lý tenant bằng control plane** — Control plane là phần quản lý tenant: tạo tenant và giữ gói dịch vụ, quota, trạng thái của từng tenant, còn data plane là phần phục vụ request của khách. Bạn giữ cho data plane vẫn chạy khi control plane sập, và dùng reconcile loop, tức vòng lặp liên tục so sánh cấu hình mong muốn với cấu hình thực tế rồi sửa chỗ lệch. Khi tạo, tạm dừng, dời hay xóa một tenant, bạn không để lại resource mồ côi (resource ở provider không còn tenant nào cần tới).
- **Hỏi agent đúng số liệu** — Trước khi làm theo một đề xuất về multi-tenant, bạn hỏi agent xem tenant lớn nhất chiếm bao nhiêu phần trăm tải, bao nhiêu câu truy vấn không đi qua lớp lọc theo tenant, tổng usage lệch bao nhiêu so với hóa đơn của provider, và job của tenant nhỏ chờ bao lâu khi tenant lớn chạy việc nặng.

Khóa học dùng Postgres làm ví dụ và không phụ thuộc vào nền tảng multi-tenant hay thư viện quota nào: code minh họa là vài đoạn TypeScript và SQL ngắn, còn policy SQL đầy đủ, câu SQL chọn job cho worker, script và cấu hình nằm trong demo kit để agent của bạn chạy.

## Khóa học cung cấp những nền tảng nào

Mọi bài trong khóa đều quay về một câu hỏi: thứ này là của tenant nào, và hệ thống có chặn được tenant khác đụng vào hay dùng quá phần của mình không? Sáu nhóm bài dưới đây đi theo bậc thang scale, tức các nấc kiến trúc mà app multi-tenant trải qua khi lớn dần. Bậc thang bắt đầu ở một app và một Postgres với cột `tenant_id` ở mọi bảng. Ở nấc cao nhất, control plane chạy thành service riêng và tenant lớn nằm trên database riêng.

Bài [Tenancy Models for a Multitenant Solution](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/considerations/tenancy-models) của Azure so sánh các mô hình, từ dùng chung mọi thứ tới mỗi tenant một bộ hạ tầng riêng. Bài [Fairness in multi-tenant systems](https://builder.aws.com/content/3Eupj3d2bo4fEvlzYbICMZNhQ3B/fairness-in-multi-tenant-systems) của AWS giải thích cách quota theo từng client giúp hệ thống ưu tiên những workload còn trong mức đã dự tính khi một client tăng tải đột biến. Bài [Static stability](https://docs.aws.amazon.com/whitepapers/latest/aws-fault-isolation-boundaries/static-stability.html) của AWS cho thấy cách giữ data plane chạy tiếp khi control plane gặp sự cố.

### 1. Dùng chung database, mỗi tenant một database riêng hay kết hợp cả hai

App có **400** tenant trên một database dùng chung, tức database mà mọi tenant cùng ghi vào. Sau khi một khách báo rằng nghi dữ liệu của mình bị lộ, agent đề xuất cho mỗi tenant một database riêng “cho an toàn”. Bạn nhờ agent tính chi phí của chính đề xuất đó. Postgres phải dự trù tối đa **8.000** connection, còn mỗi lần chạy migration, tức script đổi cấu trúc bảng, mất khoảng **4,4 giờ** vì phải chạy lần lượt trên **400** database.

Bài đặt ba mô hình cạnh nhau trên ba tenant và một bảng `contacts`, rồi so sánh chúng bằng năm tiêu chí đo được, như thời gian khôi phục riêng một tenant hay mức độ noisy neighbor. Noisy neighbor là tình trạng một tenant chiếm quá nhiều tài nguyên dùng chung nên tenant khác bị chậm.

Database riêng chặn được câu truy vấn thiếu điều kiện tenant, nhưng không chặn được việc app kết nối nhầm database hay đặt nhầm tenant cho request. Point-in-time recovery, tức đưa dữ liệu về một thời điểm trong quá khứ, chỉ làm được cho cả instance Postgres. Khi **400** database nằm chung một instance, cách này không khôi phục riêng được một tenant.

Bài kết luận rằng nên chọn mô hình theo phần tải của tenant lớn nhất và yêu cầu khôi phục. Thường app dùng chung database trước, rồi chuyển sang hybrid khi có tín hiệu đo được, như một tenant chiếm phần lớn tải hay một sự cố của tenant đó làm mọi tenant lỗi theo.

### 2. Giữ cho dữ liệu của mỗi tenant không lọt sang tenant khác

Bài nền của nhóm cho thấy tenant được đọc từ token đăng nhập rồi đi theo request qua từng tầng, tới repository, tức lớp code mà mọi hàm truy vấn theo tenant đi qua. Khi mọi hàm repository đã nhận `tenantId`, bạn nhờ agent rà codebase và thấy trong **212** câu đọc hoặc ghi vào bảng có cột `tenant_id`, có **19** câu không đi qua repository.

Chỉ cần một trong **19** câu đó thiếu điều kiện tenant là đủ để câu đó đọc được dữ liệu của mọi tenant. Bài dùng RLS để Postgres tự lọc dòng theo policy, tức điều kiện gắn vào bảng. RLS có tính chất default-deny: bảng đã bật RLS mà chưa có policy nào cho phép thì role không đọc hay sửa được dòng nào.

RLS chỉ thật sự chặn được khi đủ bốn điều kiện, và thiếu điều kiện nào thì hỏng theo kiểu riêng. Chẳng hạn app kết nối bằng role owner của bảng thì policy bị bỏ qua. Nếu tenant được đặt cho cả connection thay vì cho từng transaction, một connection lấy lại từ pool có thể còn giữ tenant của request trước. RLS cũng không thay lớp lọc ở tầng app, vì tenant bị đặt sai thì Postgres trả về đúng dữ liệu của tenant sai đó.

Bài tình huống cuối nhóm mở bằng một ticket kèm file xuất **3.100** dòng, trong đó **1.240** dòng là contact (người trong danh bạ) của một công ty khác, vì endpoint xuất file do agent viết dùng một câu SQL viết tay thiếu điều kiện `tenant_id`. Khi RLS đủ bốn điều kiện, file xuất không còn dòng nào của tenant khác, dù câu SQL viết tay cũ vẫn giữ nguyên. Bài giữ cả hai lớp: repository để code rõ ý, RLS để chặn những câu đi thẳng tới bảng.

### 3. Gói dịch vụ, quota và usage để tính tiền

Sales chốt với một khách gói Pro rằng họ được nhập tới **50.000** contact thay vì **10.000** như các khách Pro khác, và agent thêm một câu `if` theo id của khách. Vài tháng sau, code có **14** trường hợp riêng như vậy. Bài đưa gói, entitlement và giá trị riêng của tenant vào bảng, để endpoint chỉ hỏi giới hạn đang áp dụng, tức giá trị cuối cùng sau khi đã tính phần riêng của tenant.

Hai khách cùng gói Basic, cùng dùng hết **1.000** lượt hỏi trợ lý AI trong tháng, nhưng một khách tốn khoảng **750** token mỗi lượt, còn khách kia khoảng **30.000** token. Số token chênh nhau **40 lần**, và chi phí cũng chênh chừng ấy nếu hai khách dùng cùng model và cùng đơn giá mỗi token, trong khi bộ đếm quota báo hai khách dùng như nhau.

Bài chỉ ra rằng quota muốn rõ ràng thì phải nêu đơn vị, cửa sổ thời gian (khoảng thời gian dùng để đếm, ví dụ một tháng), phạm vi và việc app làm khi tenant chạm mức. Sau đó bài so sánh hai cách: đếm bằng token, hoặc vẫn đếm theo lượt nhưng đặt trần token cho mỗi lượt.

Hóa đơn tháng của provider LLM ghi **412 triệu** token, còn tổng usage app đã ghi chỉ có **375 triệu**, nên khoảng **9%** số token trên hóa đơn không gắn được cho tenant nào. Bài sửa lại metering, tức cách app ghi usage. Usage được ghi ngay tại chỗ gọi provider, mỗi lần gọi thử có một usage id riêng, còn lời gọi bị hủy hay timeout được ghi mức ước lượng rồi điều chỉnh sau, nên tổng usage chỉ còn lệch dưới **1%** so với hóa đơn.

Provider báo usage về sau **10 phút**, nên một tenant có ngân sách **2 triệu** token mỗi tháng chạm ngân sách lúc **10:00** nhưng chỉ bị chặn lúc **10:10**, khi đã dùng **2,6 triệu** token. Bài so sánh việc tạm giữ trước một phần ngân sách cho từng request với pacing, tức giữ nhịp tiêu ngân sách đều theo thời gian còn lại của kỳ. Pacing chỉ giảm mức vượt chứ không chặn đúng ở mức ngân sách.

### 4. Chia worker và hạn mức của provider công bằng giữa các tenant

Bài nền của nhóm giải thích câu lấy việc, tức câu SQL worker chạy để chọn job tiếp theo. Một khách bấm “đồng bộ lại toàn bộ” và app ghi **40.000** job vào bảng `jobs` dùng chung, còn job xuất báo cáo vài trang của một khách nhỏ chờ hơn **4 giờ**.

Bài thêm hai thứ vào câu lấy việc. Lane là nhóm việc cùng tính chất mà worker lấy riêng, như việc người dùng đang chờ hay việc chạy nền lớn. Cap là số job tối đa của một tenant được chạy cùng lúc trong một lane khi còn tenant khác đang chờ.

Worker rảnh xoay vòng trao lượt cho tenant lâu nhất chưa được cấp lượt. Đổi lại, khi đúng một tenant khác liên tục có job, đợt đồng bộ của khách lớn xong sau khoảng **8,3 giờ** thay vì khoảng **4,2 giờ** khi chưa có cap, còn job nhỏ của tenant khác được chạy sau vài giây.

Trong bài tình huống cuối nhóm, một tenant lớn nhập file **5 triệu** contact, và API danh sách contact ở các tenant nhỏ có p99 lên **2,8 giây** so với **120 ms** lúc bình thường; p99 là thời gian phản hồi mà **99%** lần gọi không vượt quá. Cùng lúc, **22%** lời gọi trợ lý AI của họ bị lỗi 429 vì vượt rate limit của provider.

Job của tenant nhỏ vẫn chờ dưới **5 giây**, nên bạn lần ngược về hai tài nguyên dùng chung đang chạm trần: disk của database bận khoảng **95%** vì bốn job nhập, còn bước phân loại contact bằng trợ lý AI dùng khoảng **85%** hạn mức **300.000** token mỗi phút của provider. Bài thêm quota tốc độ nhập cho từng tenant, chia lô nhỏ hơn, và chia hạn mức token để tenant lớn dùng tối đa khoảng **40%** khi còn tenant khác chờ.

Sau khi sửa, p99 của API danh sách contact ở tenant nhỏ không quá một phần ba giá trị trước, và tỉ lệ lỗi 429 về gần **0**. Tradeoff nằm ở tenant lớn: lần nhập **5 triệu** dòng, tính cả bước phân loại, mất khoảng **21 phút** thay vì khoảng **10 phút**.

### 5. Desired state, reconcile loop và giữ cho data plane chạy tiếp khi control plane sập

Một khách nâng lên gói Pro buổi sáng, trang quản trị báo “Pro, active” ngay, nhưng tới chiều app vẫn chặn họ ở **10.000** contact. Agent đếm được **37** tenant cùng tình trạng: job đổi gói lỗi ở bước bật trợ lý AI, và ba thành phần cùng ghi vào một cột `status`.

Bài tách desired state khỏi observed state. Desired state là cấu hình mong muốn mà tenant đã yêu cầu, observed state là cấu hình thực tế đang được áp dụng, còn status là kết luận của hệ thống về khoảng cách giữa hai cấu hình đó, kèm lý do.

Mỗi trường chỉ có một thành phần được phép ghi vào. Cấu hình nào cũng mang generation, tức số thế hệ của cấu hình, tăng thêm một mỗi lần cấu hình đổi. Nhờ số này, hệ thống phát hiện được cấu hình mới chưa được áp dụng xong.

Job thêm tên miền riêng cho tenant chỉ chạy một lần. Provider cấp chứng chỉ TLS trả về lỗi rate limit cho **6** tên miền, job bị đánh dấu `failed` rồi không chạy lại, và phải **5 ngày** sau khách mới báo lỗi chứng chỉ. Bài thay job đó bằng reconcile loop do một controller chạy: cứ mỗi **60 giây**, controller so sánh cấu hình mong muốn với cấu hình thực tế rồi làm bước còn thiếu đầu tiên.

Một bản deploy lỗi làm service quản lý tenant trả về lỗi 500 trong **20 phút**. Vì mọi request API đều hỏi service này về gói và quota, request của cả **400** tenant lỗi theo, dù database vẫn chạy bình thường. Bài cho data plane giữ bản cấu hình đã nhận của từng tenant và làm mới theo chu kỳ, nên khi control plane sập, request vẫn được phục vụ và bước kiểm tra quota vẫn chạy.

Tradeoff là mỗi lần đổi gói phải mất lâu hơn mới tới được data plane. Khi control plane chạy bình thường, phần chờ tới lượt làm mới kế tiếp kéo dài tối đa một chu kỳ làm mới, chưa tính thời gian áp dụng; nếu khách đổi gói lúc control plane đang sập, còn phải cộng thêm phần thời gian sập còn lại.

### 6. Tạm dừng, xóa, dời tenant lớn sang database riêng và tạo tenant mới

Một khách đã hủy gói ba tháng trước, vậy mà agent rà theo `tenant_id` vẫn thấy **2,1 triệu** dòng contact, **38 GB** file trong bucket, và job đồng bộ vẫn chạy cho họ mỗi đêm, vì nút “xóa tenant” chỉ đặt giá trị cho cột `deleted_at`. Bài dựng vòng đời của tenant thành các trạng thái active, suspended, offboarding và deleted, kèm bảng cho biết việc nào được chạy ở mỗi trạng thái.

Ở suspended, tức trạng thái tạm dừng, tenant bị chặn ghi và dừng job, nhưng dữ liệu vẫn được giữ và khách vẫn xuất được dữ liệu của mình. Trong khoảng chờ trước khi xóa, tenant quay về active được; hết khoảng chờ, tenant sang offboarding và không quay lại được nữa. Dữ liệu được xóa thành **210** lô, mỗi lô **10.000** dòng, có nghỉ giữa các lô để giảm ảnh hưởng lên p99 của các tenant khác.

Khách lớn nhất chiếm khoảng **30%** dữ liệu của database dùng chung. Một tính năng chỉ bật cho khách này bị lỗi, ghi lặp lịch sử hội thoại cho tới khi disk đầy, và cả **400** tenant không lưu được dữ liệu trong **35 phút**. Database riêng cho khách này phải nằm trên một instance Postgres riêng có disk riêng, vì database riêng trên cùng instance vẫn dùng chung disk.

Dời một tenant là dời mọi resource của nó, từ dòng trong bảng tới file và search index. Placement mong muốn là nơi tenant cần được chuyển tới, còn placement đang áp dụng là nơi data plane đang gửi lệnh của tenant tới; controller chỉ đổi placement đang áp dụng khi dữ liệu đã được chép và xác minh xong. Sau khi dời khoảng **270 GB**, cùng lỗi ghi lặp đó chỉ làm đầy disk của instance riêng, còn **399** tenant khác vẫn ghi bình thường.

Bài tình huống cuối khóa bắt đầu từ hóa đơn cloud cao bất thường: agent so sánh danh sách bucket của provider với bảng tenant và thấy **312** bucket không gắn với tenant nào đang hoạt động. Cùng tuần, **23** khách đăng ký xong mà tài khoản kẹt ở “đang khởi tạo” hơn **2 giờ**.

Bạn giao việc tạo tenant cho controller: request chỉ ghi tenant ở trạng thái requested rồi trả về mã 202, tức báo đã nhận yêu cầu và sẽ xử lý sau. Mỗi resource có tên suy ra từ tenant nên khách bấm lại không sinh thêm bucket, và tenant chỉ sang active khi mọi resource bắt buộc đã sẵn sàng. Sau khi sửa, số tenant kẹt quá **10 phút** và số resource mồ côi sau khoảng chờ đều về **0**.

## Vì sao 200Lab tạo ra khóa học này

Cột `tenant_id`, endpoint xuất file, bảng gói hay job nhập danh bạ, agent viết xong mỗi thứ trong vài giờ, và app chạy ổn với vài chục khách. Những gì agent khó thấy trước chỉ lộ ra khi app có vài trăm tenant chênh nhau hàng trăm lần về quy mô, như một câu SQL viết tay quên điều kiện tenant, hay một lời gọi provider bị hủy mà app không ghi usage.

Khi chuyện xảy ra, agent thường đưa ra một cách xử lý chung cho tất cả, như mỗi khách một database riêng hay thêm worker. Những cách đó tốn thêm chi phí và công vận hành, mà chưa chắc sửa được đúng chỗ hỏng. Trong khi đó, bạn là người phải trả lời ticket kèm file có danh bạ của công ty khác, giải thích vì sao khách đã trả tiền gói Pro mà vẫn bị chặn, và trả phần hóa đơn token không tính được cho khách nào.

Những hệ thống lớn cũng từng gặp đúng các loại sự cố này. Năm 2023, một bug trong thư viện client Redis khiến connection trong pool dùng chung của ChatGPT trả dữ liệu cho request của người dùng khác. Lỗi này không liên quan tới RLS, nhưng cùng kiểu với connection còn giữ tenant của request trước.

Năm 2025, một tenant trên nền tảng thương mại điện tử Norce làm quá tải cụm database dùng chung, trong khi khách dùng hạ tầng riêng (single-tenant) không bị ảnh hưởng. Cũng năm đó ở Google Cloud, lỗi của thành phần kiểm tra policy và quota nằm trên đường xử lý request làm request lỗi theo trên diện rộng.

Khóa học giúp bạn truy mỗi sự cố về đúng thành phần và đúng lựa chọn gây ra nó, như câu truy vấn nào không đi qua lớp lọc, quota đếm bằng đơn vị nào hay thành phần nào được ghi vào cột trạng thái. Nhờ đó, bạn giải thích được với team cách sửa mình chọn và tradeoff cụ thể của cách sửa đó.

Bạn cũng có cơ sở để giữ một database dùng chung cùng vài bảng quản lý tenant, chừng nào số đo chưa cho thấy lý do phải tách database. Nghĩa là tải thực tế vẫn nằm trong sức chịu tải (capacity) của database, app chưa có triệu chứng báo hiệu cần lên nấc sau của bậc thang scale, và chưa tenant nào chiếm phần lớn tải hay cần khôi phục dữ liệu riêng. Khi đó, team chưa phải vận hành control plane riêng hay hàng trăm database.

## Những vấn đề thường gặp khi app phục vụ nhiều tenant

Khi một khách thấy dữ liệu của công ty khác hay một khách lớn làm mọi khách chậm theo, team hay vá đúng chỗ vừa hỏng, thêm worker hoặc đồng ý cho mỗi khách một database riêng. Các triệu chứng dưới đây cho thấy những cách đó thường sửa nhầm chỗ, vì nguyên nhân nằm ở đường đi của dữ liệu, ở tài nguyên dùng chung đang chạm trần, hay ở thành phần giữ cấu hình của từng tenant.

- Đã thêm `WHERE tenant_id` vào endpoint xuất file bị lỗi, vậy mà **11** câu khác trong bộ **12** câu kiểm tra vẫn trả về dòng của tenant khác — repository chỉ lọc được những câu đi qua nó, còn script quản trị, job báo cáo và câu SQL viết tay đi thẳng tới bảng.
- Bảng đã bật RLS và có policy theo tenant, mà file xuất vẫn chứa **1.240** dòng của công ty khác — app kết nối bằng role owner của bảng, và owner bỏ qua policy khi bảng chưa bật chế độ áp dụng RLS cho cả owner.
- Hai khách cùng gói Basic, cùng dùng hết **1.000** lượt hỏi trợ lý AI, mà số token của một khách gấp **40 lần** khách kia — quota đếm bằng lượt trong khi provider tính tiền theo token, nên bộ đếm không thấy được chi phí thật.
- Khoảng **9%** số token trên hóa đơn của provider không gắn được cho tenant nào — app chỉ ghi usage khi lời gọi thành công, nên lời gọi bị hủy giữa lúc stream hay timeout rồi retry vẫn bị tính tiền mà không có dòng usage nào.
- Job xuất báo cáo vài trang của khách nhỏ chờ hơn **4 giờ** dù không lỗi gì — worker lấy job theo thứ tự vào hàng, nên **40.000** job của một khách lớn đứng trước mọi job vào sau.
- Job của tenant nhỏ vẫn chạy ngay, vậy mà p99 của API danh sách contact lên **2,8 giây** và **22%** lời gọi trợ lý AI bị lỗi 429 — tenant lớn đang đẩy disk của database và hạn mức token của provider chạm trần, mà lane và cap không bảo vệ hai tài nguyên dùng chung này.
- Trang quản trị báo khách đã lên gói Pro, nhưng app vẫn chặn họ ở **10.000** contact — cột `status` có ba thành phần cùng ghi vào, và không cột nào cho biết cấu hình mới đã thật sự được áp dụng hay chưa.
- Hóa đơn cloud tăng bất thường, và có **312** bucket không gắn với tenant nào đang hoạt động — request đăng ký gọi lần lượt ba provider, gateway thôi chờ sau **30 giây**, khách bấm lại, và mỗi lần thử tạo thêm một bucket có tên ngẫu nhiên.

Với từng triệu chứng này, bạn lần ra thành phần gây lỗi qua phép tính trên số liệu của ví dụ, và biết cần hỏi lại agent câu gì trước khi chọn cách sửa.

## Bạn sẽ học theo cách nào

- **Bắt đầu từ tín hiệu thật** — Mỗi bài mở bằng thứ bạn tự thấy được trên app của mình, như ticket kèm file xuất của khách, hóa đơn của provider, biểu đồ job đang chờ theo tenant hay một đề xuất của agent, kèm câu hỏi thứ đó là của tenant nào.
- **Ví dụ thu nhỏ trước, nghiệp vụ sau** — Một bảng `contacts` bốn dòng của hai tenant hay ba tenant chia nhau hai worker đủ để thấy cơ chế trước khi bài quay lại app quản lý danh bạ, và sơ đồ gọn cùng bảng trạng thái cho thấy request hay job đi qua thành phần nào.
- **Chọn phương án bằng số liệu** — Bạn quyết định cách lưu dữ liệu, đơn vị của quota, cap cho mỗi tenant hay lúc dời một tenant sang database riêng dựa trên số liệu của ví dụ, biết rõ tradeoff của từng lựa chọn và nấc nào của bậc thang scale thì chưa cần tới nó.
- **Quiz yêu cầu bạn dự đoán** — Nhiều bài cơ chế có quiz ngắn để bạn trả lời trước khi xem đáp án, như câu truy vấn thiếu điều kiện tenant trả về bao nhiêu dòng khi app kết nối bằng role owner, hay **40.000** job chạy xong sau bao lâu khi có một tenant khác liên tục có job.
- **Demo kit chạy bằng Docker** — Ba bài tình huống và hai bài cơ chế có demo kit, mỗi kit là một file Markdown để coding agent của bạn dựng hệ thống trên máy, chạy cùng một kịch bản trước và sau khi sửa rồi so sánh số đo. Lệnh, cấu hình Postgres và các provider giả lập nằm trong kit, và bạn không bắt buộc phải chạy kit.

## Khóa học dành cho ai

1. **Junior developer hoặc vibe coder** — Bạn đang xây app SaaS phục vụ nhiều doanh nghiệp trên một Postgres, phần lớn code do AI agent viết, và đã gặp hoặc sắp gặp chuyện dữ liệu lọt sang khách khác, khách lớn làm khách nhỏ chậm hay tài khoản khách mới kẹt giữa chừng; bạn cần biết vì sao hệ thống hỏng như vậy, chọn phương án nào và hỏi agent câu gì.
2. **Developer vừa nhận một đề xuất kiến trúc multi-tenant** — Agent hoặc team vừa đề xuất cho mỗi khách một database riêng, bật RLS, thêm quota hay tách phần quản lý tenant thành service riêng, và bạn cần số liệu để quyết định trước khi đồng ý.
3. **Developer đang thêm trợ lý AI mà provider tính tiền theo token** — Hóa đơn của provider lệch với usage đã ghi, khách cùng gói chênh nhau nhiều lần về chi phí, hay một tenant dùng gần hết hạn mức chung; bạn cần cách đặt quota, ghi usage và chia hạn mức theo tenant.

## Những điều cần lưu ý

- **Kiến thức nền cần có** — Bạn chỉ cần từng làm app nhận request và ghi vào Postgres trong transaction, kể cả khi code do agent viết, và đọc được TypeScript, SQL cùng dòng lỗi trong log; không cần từng vận hành Kubernetes hay Terraform.
- **Nên học thêm các khóa System Design liên quan** — Hai bài nền lấy từ khóa “Thiết kế API giữa các service — error model, versioning và contract test” và khóa “Xử lý timeout, retry và crash — idempotency, outbox, lease và reconciliation”, đặt ngay trước bài cần chúng. Hai khóa “Thiết kế hệ thống chịu tải cao — rate limiting, load shedding và circuit breaker” và “Thiết kế database nhiều shard — shard key, hot key, lookup index và reshard” không phải điều kiện để học khóa này, nhưng giúp bạn nắm đầy đủ hơn về rate limit, hạn mức riêng cho từng client gọi API và cách chép dữ liệu trong lúc app vẫn ghi. Ở khóa này, các phần đó chỉ được nhắc một câu.
- **Không hướng dẫn cấu hình một nền tảng multi-tenant cụ thể** — Reconcile loop được viết tay trên Postgres, Kubernetes operator chỉ được gọi tên làm ví dụ quen thuộc, còn policy SQL, câu lấy việc đầy đủ, script và cấu hình nằm trong demo kit cho agent; đổi lại, bạn hiểu cơ chế đủ rõ để biết agent và công cụ đang quyết định thay bạn những gì.
- **Số liệu trong ví dụ do bài tự đặt** — Số tenant, số token hay p99 trong bài được chọn để các phép tính khớp nhau, không phải số đo từ production của một công ty nào; khi áp dụng, bạn nhờ agent đo trên hệ thống của mình, như tỉ lệ tải của tenant lớn nhất hay độ lệch giữa usage và hóa đơn, rồi đưa vào cùng cách tính.

## Bắt đầu từ đâu

Bài đầu tiên mở bằng bốn sự cố của một app vừa lên **400** khách hàng, cùng đề xuất chung của agent: mỗi khách một database riêng. Bài chỉ ra control plane và data plane đang chạy chung trong app của bạn, rồi nêu ba câu hỏi cần trả lời trước khi làm theo đề xuất và bốn nấc kiến trúc của app multi-tenant. Xếp được app của mình vào một nấc, bạn sẽ biết nên đọc kỹ nhóm bài nào trước và cơ chế nào app chưa cần.

## Chương trình

- Một app có nhiều doanh nghiệp cùng dùng thì cần thêm những gì? (miễn phí)

### Giữ cho dữ liệu của mỗi tenant không lọt sang tenant khác

- Mỗi khách hàng một database riêng có thật sự an toàn hơn không? (miễn phí)
- Người bán đọc được đơn hàng của người bán khác qua API nội bộ · quiz
- Vì sao code đã lọc theo tenant vẫn để lọt dữ liệu của khách khác? · quiz
- Tình huống: file xuất của khách chứa dữ liệu của công ty khác · kit cho agent

### Quota, metering và chia tài nguyên công bằng giữa các tenant

- Vì sao tăng giới hạn cho một khách mà phải sửa code và deploy? · quiz
- Cùng gói, cùng số lượt hỏi AI mà số token chênh nhau 40 lần · quiz
- Vì sao usage đã ghi thấp hơn số token trên hóa đơn của provider? · quiz · kit cho agent
- Ghi mỗi lượt chạy cron thành một dòng để không lượt nào bị lỡ hay chạy chồng · quiz · kit cho agent
- Job của khách nhỏ chờ hàng giờ sau 40.000 job của một khách lớn · quiz · kit cho agent
- Tình huống: tenant nhập 5 triệu dòng làm chậm mọi tenant khác · kit cho agent

### Tạo, đổi cấu hình, tạm dừng và xóa tenant bằng control plane

- Trang quản trị báo gói Pro nhưng khách vẫn bị giới hạn như gói cũ · quiz
- Job thêm tên miền riêng thất bại một lần rồi không được chạy lại · quiz
- Control plane sập làm request của mọi khách đều lỗi theo · quiz
- Khách đã hủy gói từ ba tháng trước mà dữ liệu vẫn còn khắp nơi · quiz
- Sự cố của một tenant lớn khiến mọi tenant không ghi được dữ liệu
- Tình huống: tạo tenant dở dang và để lại resource mồ côi · kit cho agent
