Introduction
Multi-tenant SaaS applications serve multiple customers from a single codebase and infrastructure. Building a robust multi-tenant system requires careful consideration of data isolation, performance, scalability, and security. Many SaaS companies start with single-tenant architecture and struggle to migrate to multi-tenant later.
This comprehensive guide covers multi-tenant SaaS architecture, tenant isolation strategies, and scaling patterns used by companies serving thousands of customers.
Core Concepts
Tenant
Individual customer or organization using the SaaS application.
Tenant Isolation
Ensuring data and resources of one tenant don’t affect others.
Row-Level Security (RLS)
Database-level enforcement of tenant data boundaries.
Tenant Context
Information identifying current tenant in request.
Shared Infrastructure
Single database/servers serving multiple tenants.
Dedicated Infrastructure
Separate database/servers per tenant.
Hybrid Model
Mix of shared and dedicated infrastructure.
Tenant Routing
Directing requests to appropriate tenant resources.
Data Residency
Storing tenant data in specific geographic regions.
Compliance Isolation
Ensuring regulatory requirements per tenant.
Multi-Tenant Architecture Patterns
Pattern 1: Shared Database, Shared Schema
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Single Database โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Tenant A Data โ Tenant B Data โ Tenant C โ โ
โ โ (tenant_id=1) โ (tenant_id=2) โ (id=3) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Cost: Lowest โ
โ Isolation: Lowest โ
โ Complexity: Lowest โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Shared database with tenant_id column
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
tenant_id = db.Column(db.Integer, nullable=False)
email = db.Column(db.String(120), unique=False)
name = db.Column(db.String(120))
__table_args__ = (
db.Index('idx_tenant_id', 'tenant_id'),
db.Index('idx_tenant_email', 'tenant_id', 'email'),
)
# Always filter by tenant_id
def get_user(user_id, tenant_id):
return User.query.filter_by(
id=user_id,
tenant_id=tenant_id
).first()
# Middleware to set tenant context
@app.before_request
def set_tenant_context():
# Extract tenant from subdomain or header
tenant_id = extract_tenant_id(request)
g.tenant_id = tenant_id
# Decorator to enforce tenant isolation
def require_tenant_isolation(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Verify user belongs to tenant
user = get_current_user()
if user.tenant_id != g.tenant_id:
return jsonify({"error": "Unauthorized"}), 403
return f(*args, **kwargs)
return decorated_function
@app.route("/api/users/<user_id>")
@require_tenant_isolation
def get_user_api(user_id):
user = get_user(user_id, g.tenant_id)
return jsonify(user.to_dict())
Pattern 2: Shared Database, Separate Schema
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Single Database โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ Schema A โ โ Schema B โ โ Schema C โ โ
โ โ(Tenant A)โ โ(Tenant B)โ โ(Tenant C)โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ
โ Cost: Low โ
โ Isolation: Medium โ
โ Complexity: Medium โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Separate schema per tenant
def create_tenant_schema(tenant_id):
schema_name = f"tenant_{tenant_id}"
# Create schema
db.session.execute(f"CREATE SCHEMA {schema_name}")
# Create tables in schema
db.session.execute(f"""
CREATE TABLE {schema_name}.users (
id SERIAL PRIMARY KEY,
email VARCHAR(120),
name VARCHAR(120)
)
""")
db.session.commit()
# Query with schema
def get_user_from_schema(user_id, tenant_id):
schema_name = f"tenant_{tenant_id}"
result = db.session.execute(f"""
SELECT * FROM {schema_name}.users WHERE id = {user_id}
""")
return result.fetchone()
# Middleware to set schema
@app.before_request
def set_schema_context():
tenant_id = extract_tenant_id(request)
schema_name = f"tenant_{tenant_id}"
# Set search_path for PostgreSQL
db.session.execute(f"SET search_path TO {schema_name}, public")
Pattern 3: Dedicated Database per Tenant
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Database A โ โ Database B โ โ Database C โ
โ (Tenant A) โ โ (Tenant B) โ โ (Tenant C) โ
โ โ โ โ โ โ
โ - Users โ โ - Users โ โ - Users โ
โ - Orders โ โ - Orders โ โ - Orders โ
โ - Products โ โ - Products โ โ - Products โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
Cost: Highest
Isolation: Highest
Complexity: Highest
# Tenant database registry
TENANT_DATABASES = {
1: "postgresql://user:[email protected]/tenant_1",
2: "postgresql://user:[email protected]/tenant_2",
3: "postgresql://user:[email protected]/tenant_3"
}
# Get database connection for tenant
def get_tenant_db_connection(tenant_id):
db_url = TENANT_DATABASES.get(tenant_id)
if not db_url:
raise ValueError(f"No database for tenant {tenant_id}")
return create_engine(db_url)
# Query with tenant-specific connection
def get_user_from_tenant_db(user_id, tenant_id):
engine = get_tenant_db_connection(tenant_id)
with engine.connect() as conn:
result = conn.execute(
text("SELECT * FROM users WHERE id = :id"),
{"id": user_id}
)
return result.fetchone()
Row-Level Security (RLS)
PostgreSQL RLS Implementation
def enable_rls_for_tenant(tenant_id):
"""Enable Row-Level Security for tenant"""
# Create policy for tenant
db.session.execute(f"""
CREATE POLICY tenant_isolation_policy ON users
USING (tenant_id = current_setting('app.current_tenant_id')::int)
WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::int)
""")
# Enable RLS
db.session.execute("ALTER TABLE users ENABLE ROW LEVEL SECURITY")
db.session.commit()
# Middleware to set tenant context
@app.before_request
def set_rls_context():
tenant_id = extract_tenant_id(request)
# Set tenant context for RLS
db.session.execute(
f"SET app.current_tenant_id = {tenant_id}"
)
# Query automatically filtered by RLS
def get_all_users():
# RLS automatically filters by tenant_id
return User.query.all()
Tenant Routing
Subdomain-Based Routing
def extract_tenant_from_subdomain(request):
"""Extract tenant from subdomain"""
# acme.example.com -> tenant_id for acme
host = request.host
subdomain = host.split('.')[0]
# Look up tenant by subdomain
tenant = db.session.execute(
text("SELECT id FROM tenants WHERE subdomain = :subdomain"),
{"subdomain": subdomain}
).fetchone()
return tenant[0] if tenant else None
# Middleware
@app.before_request
def route_by_subdomain():
tenant_id = extract_tenant_from_subdomain(request)
if not tenant_id:
return jsonify({"error": "Invalid tenant"}), 404
g.tenant_id = tenant_id
Header-Based Routing
def extract_tenant_from_header(request):
"""Extract tenant from header"""
# X-Tenant-ID: 123
tenant_id = request.headers.get("X-Tenant-ID")
if not tenant_id:
return None
# Verify tenant exists
tenant = db.session.execute(
text("SELECT id FROM tenants WHERE id = :id"),
{"id": int(tenant_id)}
).fetchone()
return tenant[0] if tenant else None
# Middleware
@app.before_request
def route_by_header():
tenant_id = extract_tenant_from_header(request)
if not tenant_id:
return jsonify({"error": "Invalid tenant"}), 400
g.tenant_id = tenant_id
Scaling Multi-Tenant Systems
Horizontal Scaling
# Load balancer configuration
TENANT_SHARDS = {
"shard_1": ["tenant_1", "tenant_2", "tenant_3"],
"shard_2": ["tenant_4", "tenant_5", "tenant_6"],
"shard_3": ["tenant_7", "tenant_8", "tenant_9"]
}
def get_shard_for_tenant(tenant_id):
"""Get shard for tenant"""
for shard, tenants in TENANT_SHARDS.items():
if tenant_id in tenants:
return shard
return None
def route_to_shard(tenant_id):
"""Route request to appropriate shard"""
shard = get_shard_for_tenant(tenant_id)
if shard == "shard_1":
return "app1.example.com"
elif shard == "shard_2":
return "app2.example.com"
elif shard == "shard_3":
return "app3.example.com"
Caching Strategy
from redis import Redis
redis_client = Redis(host="localhost", port=6379)
def get_user_cached(user_id, tenant_id):
"""Get user with caching"""
cache_key = f"tenant:{tenant_id}:user:{user_id}"
# Check cache
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# Query database
user = get_user(user_id, tenant_id)
# Cache result
redis_client.setex(
cache_key,
3600, # 1 hour TTL
json.dumps(user.to_dict())
)
return user
Monitoring Multi-Tenant Systems
class TenantMetrics:
def __init__(self):
self.metrics = {}
def record_request(self, tenant_id, endpoint, duration_ms):
"""Record request metrics"""
if tenant_id not in self.metrics:
self.metrics[tenant_id] = {
"requests": 0,
"total_duration": 0,
"endpoints": {}
}
self.metrics[tenant_id]["requests"] += 1
self.metrics[tenant_id]["total_duration"] += duration_ms
if endpoint not in self.metrics[tenant_id]["endpoints"]:
self.metrics[tenant_id]["endpoints"][endpoint] = {
"count": 0,
"total_duration": 0
}
self.metrics[tenant_id]["endpoints"][endpoint]["count"] += 1
self.metrics[tenant_id]["endpoints"][endpoint]["total_duration"] += duration_ms
def get_tenant_stats(self, tenant_id):
"""Get tenant statistics"""
if tenant_id not in self.metrics:
return None
stats = self.metrics[tenant_id]
avg_duration = stats["total_duration"] / stats["requests"]
return {
"tenant_id": tenant_id,
"total_requests": stats["requests"],
"avg_duration_ms": avg_duration,
"endpoints": stats["endpoints"]
}
# Middleware to track metrics
@app.before_request
def start_timer():
g.start_time = time.time()
@app.after_request
def record_metrics(response):
duration_ms = (time.time() - g.start_time) * 1000
tenant_id = g.get("tenant_id")
endpoint = request.endpoint
metrics.record_request(tenant_id, endpoint, duration_ms)
return response
Comparison: Shared vs Dedicated
| Aspect | Shared | Dedicated |
|---|---|---|
| Cost | Low | High |
| Isolation | Low | High |
| Complexity | Low | High |
| Scalability | Medium | High |
| Data Residency | Difficult | Easy |
| Compliance | Difficult | Easy |
| Performance | Variable | Consistent |
External Resources
SaaS Architecture
Database Design
Conclusion
Multi-tenant SaaS architecture requires careful planning of data isolation, routing, and scaling. Start with shared database and row-level security for simplicity, then migrate to dedicated infrastructure as you scale.
Choose the right isolation model for your compliance and performance requirements. Monitor tenant metrics continuously and scale proactively.
Comments