Tenant Isolation in a Skewed Multi-Tenant SaaS
A B2B SaaS platform serves 6,000 tenants. Usage is highly skewed: the largest tenant accounts for about 35% of all documents, roughly 120 million, while the median tenant has about 2,000 documents. The application currently uses one shared collection with a tenantId field on every document and compound indexes beginning with tenantId. Large-tenant queries perform well, but index size is large, small tenants experience noisy-neighbor latency during large imports, and a compliance requirement now mandates that a single tenant's data can be exported and restored independently without affecting others. The team is considering collection-per-tenant versus keeping the shared collection. They must also model tenant settings, users, and projects. The operations team is small and wants to avoid thousands of collections if possible.
1. Compare a shared collection with tenantId against collection-per-tenant. Recommend a design that addresses the 35% tenant, noisy-neighbor latency, and per-tenant export/restore.
Read the worked answer
A shared collection is operationally simple and works well when every query includes tenantId and indexes lead with it. It keeps one schema, one set of indexes, and straightforward code. The problems are real for this workload: the largest tenant dominates index size and cache, and a large import can evict data used by small tenants. Per-tenant export is logical rather than physical, but it is still possible by querying tenantId.
Collection-per-tenant gives physical isolation, smaller per-tenant indexes, per-tenant drop or restore, and natural noisy-neighbor containment. But 6,000 collections impose operational overhead on a small team and complicate cross-tenant reporting.
A hybrid recommendation fits the skew: keep most small tenants in a shared collection with tenantId and compound indexes leading with tenantId, and move only very large tenants, for example those above 5 million documents, to dedicated collections or databases. Maintain a tenant registry that maps tenantId to its collection. Use the same document shape and tenantId field in dedicated collections so application code does not branch unnecessarily. This isolates the 35% tenant and supports independent export/restore where it matters, while avoiding thousands of collections for the long tail. If compliance requires physical isolation for every tenant, then collection-per-tenant for all is justified, but the operational cost must be accepted explicitly.
2. Model tenant settings, users, and projects. Where do you embed and where do you reference? Give indexes.
Read the worked answer
Tenant settings are one-to-one with a tenant, usually small, and read with tenant profile data, so embed them in the tenant document. If settings become large or change independently, move them to a separate settings document with tenantId, but that is not the default.
Users need global login by email, so use a users collection with a unique index on email. Include tenantId and roles, or a memberships array or separate memberships collection if a user can belong to multiple tenants. Embedding users inside a tenant would make global authentication awkward and risk unbounded arrays.
Projects are one-to-many from a tenant and should be a separate projects collection with tenantId, ownerId, name, and createdAt. Project members can be embedded as a bounded array of userId and role if membership is small, for example under 100, but user profile data stays referenced. Do not embed an unbounded list of project activity or audit events.
Indexes: unique { email: 1 } on users; { tenantId: 1, role: 1 } for tenant user administration; { tenantId: 1, createdAt: -1 } for project lists; and { tenantId: 1, ownerId: 1 } for ownership queries. In dedicated collections, keep tenantId for uniform code even though it is constant.
3. How would you route queries to the correct collection and prevent cross-tenant data leaks?
Read the worked answer
Derive the tenant context from the authenticated session or token, never from a client-supplied tenantId parameter. A repository or data-access layer consults the tenant registry and returns the correct collection handle. Every query still includes tenantId as a defense-in-depth predicate, even in a dedicated collection, so a routing bug does not automatically expose another tenant's data.
Add automated tests that attempt cross-tenant reads and writes, and audit logs for administrative access. For export and restore, use the registry to locate the tenant's collection or to issue a tenantId-filtered export. For dedicated tenants, a physical collection dump is possible; for shared tenants, use a filtered export and validate that the filter is applied.
Keep the routing layer small and centralized, because scattered collection-name logic is where leaks happen. Also monitor query plans to ensure tenantId remains in the index prefix in the shared collection. The combination of authenticated tenant context, registry routing, and an always-present tenantId predicate gives both isolation and defense in depth.