Courses › System Design › Patterns

Multi-Tenancy

Isolation models, tenant-scoped data, partitioning by tenant, noisy neighbors and tiered tenants.

A multi-tenant system serves many customers (tenants) — companies, workspaces, organizations — from shared infrastructure. Almost every B2B SaaS product works this way: Slack workspaces, Shopify stores, GitHub organizations. The questions it raises come up constantly in interviews: How do you keep one customer's data away from another? How do you stop one huge or noisy customer from hurting everyone else? How do you handle tenants that range from 3 users to 300,000?

The problem

  • Isolation: tenant A must never see tenant B's data. A single missing filter is a serious security incident.
  • Noisy neighbors: one tenant running a huge import or a runaway integration can exhaust shared resources.
  • Skew: tenant sizes follow a power law. A design that works for the median tenant may break for the largest.
  • Per-tenant needs: data residency (EU data stays in the EU), dedicated capacity for enterprise plans, per-tenant backups and deletion.

Isolation models

[!TIP] Tier it A strong interview answer is usually hybrid: "Small tenants share pooled shards. Large tenants get their own shard, and enterprise customers with residency requirements get a dedicated cell in their region. A routing table maps each tenant to its location."

Enforcing data isolation

  • Put tenant_id in every table and make it the leading column of primary keys and indexes — (tenant_id, document_id) — so queries are efficient and naturally scoped.
  • Enforce scoping centrally, not in every query by hand: a data-access layer that requires a tenant context, or database row-level security (PostgreSQL RLS) keyed on a session variable.
  • Derive the tenant from the authenticated identity, never from a request parameter the client controls. See Authentication & Authorization.
  • Scope caches, search indexes, queues and blob paths by tenant too (cache key = tenant:42:user:7, s3://bucket/tenant-42/...). Cross-tenant leaks often happen in caches, not databases.
  • Use per-tenant encryption keys when customers require it, which also makes deletion verifiable (destroy the key).

Partitioning by tenant

  • Shard by tenant ID so each tenant's data sits together and most queries hit one shard. See Sharding.
  • Keep a tenant directory (tenant → shard / cell / region) rather than pure hashing, so you can move a growing tenant to its own shard without rehashing everyone.
  • Very large tenants may need to be split within the tenant (by channel, project or user), while still keeping a tenant-level directory.
[Request + auth token] -> [Gateway: resolve tenant] -> [Tenant directory] -> [Shard / cell for that tenant]

Handling noisy neighbors

  • Per-tenant rate limits and quotas at the gateway — requests per second, concurrent jobs, storage. See Throttling with Rate Limits.
  • Fair queueing for background work: per-tenant queues or weighted round-robin, so one tenant's 10M-row import doesn't block everyone else's jobs.
  • Bulkheads: separate worker pools or connection pools for the largest tenants. See Resilience Patterns.
  • Cells: split tenants into independent cells (complete copies of the stack) so any incident affects a fraction of customers.
  • Per-tenant metrics to find the noisy tenant fast — label by tenant tier rather than tenant ID in your time-series database to keep cardinality manageable, and log the tenant ID.

Operations

  • Migrations run across every tenant schema or shard; roll them out gradually with the same discipline as code. See Zero-Downtime Migrations.
  • Tenant moves (to a dedicated shard or another region) use the copy → dual-write → cut over → clean up pattern.
  • Deletion and export must cover every store: database, blobs, search, caches, backups and analytics.

[!INTERVIEW] Slack-style workspaces "Every table is keyed by (workspace_id, ...), and a data-access layer refuses queries without a workspace context. Most workspaces share pooled shards; a directory maps each workspace to its shard, so when a workspace grows past ~50k users we migrate it to a dedicated shard. Gateways enforce per-workspace rate limits, and background jobs use per-workspace fair queues so one huge export doesn't delay everyone's notifications."