What is Multi-Tenancy?
Multi-tenancy means a single application instance serves multiple customers (tenants), with each tenant’s data isolated from others. SaaS products like Salesforce, Slack, and Atlassian are multi-tenant. The key design question: how do you isolate tenant data — at the database level, schema level, or row level? Each approach makes different trade-offs between isolation, cost, and operational complexity.
Three Isolation Models
Database-per-tenant: each tenant gets a dedicated database. Strongest isolation, simple queries (no tenant_id filter needed), easy to migrate/backup individual tenants. Con: expensive at scale (1000 tenants = 1000 DBs), complex provisioning, hard to run cross-tenant analytics.
Schema-per-tenant: each tenant gets a dedicated schema within the same database instance (PostgreSQL schemas). Good isolation, shared DB infrastructure. Con: schema migrations must run for every tenant, harder to manage at thousands of tenants.
Shared database, row-level isolation: all tenants share tables; every row has a tenant_id foreign key. Cheapest, easiest to operate. Con: risk of data leakage if tenant_id filters are missed, query performance degrades without proper indexing, noisy neighbor problem (one tenant’s heavy queries affect others).
Row-Level Isolation (Most Common for SaaS)
-- Every table has tenant_id
CREATE TABLE projects (
project_id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),
name VARCHAR,
created_at TIMESTAMP
);
-- Composite index for tenant queries
CREATE INDEX idx_projects_tenant ON projects(tenant_id, created_at DESC);
-- Row Level Security (PostgreSQL RLS)
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
PostgreSQL Row Level Security (RLS) enforces isolation at the DB level — even if application code forgets the WHERE tenant_id filter, the policy blocks access. Set the tenant context at query time:
def execute_query(query, tenant_id, params):
with db.connection() as conn:
conn.execute("SET LOCAL app.current_tenant_id = %s", (str(tenant_id),))
return conn.execute(query, params)
Tenant Routing
Identify the tenant from the request. Common methods:
# 1. Subdomain: acme.app.example.com → tenant=acme
tenant = request.host.split('.')[0]
# 2. Custom domain: acme.com → look up in TenantDomain table
tenant = TenantDomain.get(domain=request.host).tenant
# 3. JWT claim: token contains tenant_id
tenant = jwt.decode(token)['tenant_id']
# 4. API key: each key is scoped to a tenant
tenant = ApiKey.get(key=request.headers['X-API-Key']).tenant
Data Model for Tenants
Tenant(tenant_id UUID, name VARCHAR, slug VARCHAR UNIQUE,
plan ENUM(FREE, STARTER, PROFESSIONAL, ENTERPRISE),
status ENUM(ACTIVE, SUSPENDED, CANCELLED),
max_users INT, max_storage_gb INT,
created_at, billing_customer_id VARCHAR)
TenantDomain(domain VARCHAR PRIMARY KEY, tenant_id UUID)
-- for custom domain support
TenantUser(tenant_id UUID, user_id UUID, role ENUM(OWNER, ADMIN, MEMBER),
invited_at, joined_at,
PRIMARY KEY (tenant_id, user_id))
Noisy Neighbor Mitigation
In shared infrastructure, one tenant’s heavy usage degrades others. Mitigations: (1) Per-tenant rate limits on API endpoints. (2) Query timeouts: kill queries running longer than N seconds. (3) Resource quotas: max_storage_gb, max_api_calls_per_day enforced at the application layer. (4) Connection pooling per tenant: PgBouncer pool_mode=transaction with per-tenant limits. (5) For enterprise tenants on paid plans: dedicated DB replicas for read-heavy workloads.
Key Design Decisions
- PostgreSQL RLS for row-level isolation — DB-enforced, fails safe if application code omits tenant filter
- Composite index (tenant_id, …) on all tenant tables — required for query performance; full table scans without it
- Tenant context from request, not from user — users can belong to multiple tenants; the request determines which tenant is active
- Plan-based resource limits — prevent noisy neighbor without operational complexity of separate DBs
- Slug for subdomain routing — human-readable, URL-safe, must be globally unique across all tenants
Multi-tenant SaaS architecture is a common topic in Atlassian system design interview questions.
Multi-tenancy and tenant isolation design is discussed in Shopify system design interview guide.
Multi-tenant data isolation and resource quotas are covered in Databricks system design interview preparation.
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering