Multi-Tenancy Architecture
Design multi-tenant applications with shared or isolated databases, tenant-aware routing, and data isolation strategies.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
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
- 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 |
Best Practices
- 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 dynamic 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
Frequently Asked Questions
Q: Can I migrate from shared DB to schema-per-tenant later? A: Yes, but it requires a significant migration. Start with tenant_id columns and RLS even if you plan to split later.
Q: How do I handle tenant-specific customizations? A: Use feature flags per tenant, white-label configuration, or metadata-driven UI. Avoid separate code branches.
Q: Does GDPR affect multi-tenancy design? A: 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 comprehensive template for team engineering handbooks covering standards, workflows, onboarding, and operational practices.
GuideREST API Design Guide
A comprehensive 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.