Multi-Tenancy Architecture
Design multi-tenant applications with shared or isolated databases, tenant-aware routing, and data isolation strategies.
Overview
Multi-tenancy is an architecture where a single software instance serves multiple customers (tenants) while keeping their data and configuration isolated. The trade-off is between operational simplicity (shared everything) and data isolation (separate everything). Choosing the right model affects scalability, security, and compliance.
When to Use
Use this resource when:
- Building SaaS applications serving multiple organizations
- Meeting compliance requirements (SOC 2, HIPAA) that mandate data segregation. See API Security Checklist for what works in compliance.
- Optimizing infrastructure costs by sharing compute across tenants
- Scaling from hundreds to thousands of tenants with predictable performance
Solution
Shared Database with Tenant ID (PostgreSQL)
-- Row-Level Security ensures tenant isolation
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
amount DECIMAL(10,2) NOT NULL
);
-- Enable RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Policy: tenants can only see their own data
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.current_tenant')::UUID);
Tenant-Aware Middleware (Node.js)
function tenantMiddleware(req, res, next) {
const tenantId = req.headers['x-tenant-id'] || req.subdomain;
if (!tenantId) {
return res.status(400).json({ error: 'Tenant ID required' });
}
// Set tenant context for this request
req.tenantId = tenantId;
// Apply to database connection
db.query("SET app.current_tenant = $1", [tenantId]);
next();
}
Schema-Per-Tenant Migration
from sqlalchemy import create_engine, MetaData
def migrate_tenant_schema(tenant_id: str):
engine = create_engine("postgresql://user:pass@localhost/db")
with engine.begin() as conn:
conn.execute("CREATE SCHEMA IF NOT EXISTS tenant_{}".format(tenant_id))
# Run migrations within tenant schema
metadata = MetaData(schema="tenant_{}".format(tenant_id))
metadata.create_all(conn)
Explanation
Three multi-tenancy models:
| Model | Isolation | Cost | Complexity |
|---|---|---|---|
| Shared DB + Tenant ID | Low (RLS needed) | Lowest | Low |
| Schema-per-tenant | Medium | Medium | Medium |
| Database-per-tenant | High | Highest | High |
Tenant resolution strategies:
- Subdomain: tenant1.app.com, tenant2.app.com
- Path: app.com/tenant1/, app.com/tenant2/
- Header: X-Tenant-ID in API requests
- JWT claim: tenant embedded in auth token
Variants
| Approach | Best For | Trade-off |
|---|---|---|
| Shared everything | Early-stage SaaS | Simplest; weakest isolation |
| Shared compute, isolated storage | Mid-market SaaS | Balance of cost and compliance |
| Fully isolated | Enterprise/regulated | Highest cost; strongest isolation |
| Cell-based | Global scale | Shards tenants across regions |
What Works
- Never trust tenant ID from user input: Always resolve from authenticated context
- Index tenant_id first: Every query filters by tenant; make it the leading column
- Use connection pooling carefully: Schema-per-tenant requires live schema switching
- Backup per tenant: Schema-per-tenant makes pg_dump per-schema trivial
- Resource quotas: Limit CPU, storage, and API rate per tenant to prevent noisy neighbors
Common Mistakes
- Missing tenant filter: One forgotten WHERE tenant_id = $1 exposes all customer data
- Caching without tenant scoping: Shared cache keys leak data across tenants
- Background jobs without tenant context: Scheduled tasks must run for each tenant separately
- Hard-coded schemas: Mixing tenant data in application code creates security holes
- No tenant-aware logging: Debugging production issues requires filtering logs by tenant
Additional Common Mistakes
-
Cross-tenant queries in analytics. Reporting queries that aggregate across tenants without filtering expose data. Use separate analytics warehouses per tenant or enforce tenant_id filters in every BI query.
-
Shared sequences and auto-increment. Using a shared
SERIALprimary key across tenants in a shared database creates contention and leaks tenant scale information. Use UUIDs instead:
-- Bad: shared sequence, contention
CREATE TABLE orders (id SERIAL PRIMARY KEY, tenant_id UUID, ...);
-- Good: UUID, no contention, no information leak
CREATE TABLE orders (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID, ...);
- Ignoring tenant lifecycle in CI/CD. Deployments that run schema migrations must handle all tenant schemas. A migration that works for one tenant may fail for another with different data volume. Test migrations against the largest tenant first.
Frequently Asked Questions
Can I migrate from shared DB to schema-per-tenant later?
Yes, but it requires a major migration. Start with tenant_id columns and RLS even if you plan to split later.
How do I handle tenant-specific customizations?
Use feature flags per tenant, white-label configuration, or metadata-driven UI. Avoid separate code branches.
Does GDPR affect multi-tenancy design?
Yes. Right to erasure is simpler with schema-per-tenant (drop schema) than with shared tables (delete rows across many tables).
Related Resources
ADR Template
A reusable template for Architecture Decision Records that capture context, decision, and consequences.
DocDatabase Schema Documentation Template
A template for documenting database schemas with entity relationships, field definitions, and migration history.
DocEngineering Handbook Template
A template for documenting team culture, development processes, technical standards, and operational practices in a single referenceable handbook.
GuideREST API Design Guide
A thorough guide to designing clean, scalable, and maintainable REST APIs.
GuideDomain-Driven Design (DDD): A Practical Guide
Learn DDD fundamentals: bounded contexts, entities, value objects, aggregates, and how to model complex business domains in code.
RecipeDesign a Scalable API Gateway for Microservices
Build an API gateway that routes requests, handles authentication, rate limiting, caching, and protocol translation between clients and backend microservices.