Introduction
Software as a Service (SaaS) applications serve multiple customers from a single deployment. This requires careful architectural decisions around resource sharing, data isolation, and cost optimization. Multi-tenant architecture enables organizations to build scalable SaaS products that efficiently serve diverse customer bases while maintaining security and performance.
This article explores multi-tenant architecture patterns, implementation strategies, and best practices for building production-ready SaaS applications.
Understanding Multi-Tenancy
What is Multi-Tenancy?
Multi-tenancy is an architecture where a single instance of software serves multiple customers (tenants). Each tenant’s data is isolated while sharing underlying compute resources.
Key Benefits
- Cost efficiency: Share infrastructure across tenants
- Simplified maintenance: Single deployment for all tenants
- Scalability: Add tenants without provisioning new infrastructure
- Resource optimization: Dynamic allocation based on demand
Tenancy Models
Model 1: Shared Everything
All tenants share the same database, application instance, and storage.
# Single database with tenant column
Table: Orders
- id: UUID
- tenant_id: UUID # All queries filter by this
- customer_id: UUID
- total: DECIMAL
- created_at: TIMESTAMP
Pros: Maximum resource sharing, lowest cost Cons: Complex queries, noisy neighbor problems, security concerns Use cases: Low-security workloads, cost-sensitive applications
Model 2: Shared Database, Separate Schemas
Tenants share a database but have isolated schemas.
-- Tenant A's schema
CREATE SCHEMA tenant_a;
CREATE TABLE tenant_a.users (...);
-- Tenant B's schema
CREATE SCHEMA tenant_b;
CREATE TABLE tenant_b.users (...);
Pros: Better isolation than column-based, simpler queries Cons: Schema management complexity, migration challenges Use cases: Moderate isolation requirements
Model 3: Separate Databases
Each tenant gets its own database.
# Connection routing
tenant-a.db.company.com -> Database: tenant_a
tenant-b.db.company.com -> Database: tenant_b
Pros: Strong isolation, independent scaling Cons: Higher infrastructure costs, management overhead Use cases: Enterprise customers requiring guarantees
Model 4: Separate Infrastructure
Complete isolation with dedicated resources per tenant.
# Per-tenant Kubernetes namespace
apiVersion: v1
kind: Namespace
metadata:
name: tenant-a
---
apiVersion: v1
kind: Namespace
metadata:
name: tenant-b
Pros: Maximum isolation, no noisy neighbors Cons: Highest cost, operational complexity Use cases: Regulated industries, large enterprises
Database Design Patterns
Tenant Context Pattern
Add tenant ID to all data operations:
# Middleware that injects tenant context
class TenantMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
tenant_id = self.extract_tenant_id(request)
TenantContext.set_current(tenant_id)
response = self.get_response(request)
TenantContext.clear()
return response
# Repository automatically filters by tenant
class OrderRepository:
def get_all(self):
tenant_id = TenantContext.get_current()
return self.db.query(
"SELECT * FROM orders WHERE tenant_id = ?",
tenant_id
)
Row-Level Security
Database-enforced tenant isolation:
-- PostgreSQL: Create policy
CREATE POLICY tenant_isolation_policy ON orders
USING (tenant_id = current_setting('app.tenant_id'));
-- Set tenant context
SET app.tenant_id = 'tenant-uuid';
SELECT * FROM orders; -- Returns only tenant's data
Metadata-Driven Configuration
Flexible tenant-specific settings:
# Tenant configuration
tenants:
- id: tenant-a
name: Company A
plan: enterprise
features:
custom_branding: true
api_access: true
sso_enabled: true
limits:
max_users: 1000
storage_gb: 100
- id: tenant-b
name: Company B
plan: standard
features:
custom_branding: false
api_access: false
limits:
max_users: 100
storage_gb: 10
Application Architecture
Tenant Routing
Route requests to appropriate tenant context:
// API Gateway tenant resolution
async function resolveTenant(request: Request): Promise<string> {
// Method 1: Subdomain
const host = request.headers.get('host');
const subdomain = host?.split('.')[0];
if (subdomain && await isValidTenant(subdomain)) {
return subdomain;
}
// Method 2: Header
const tenantHeader = request.headers.get('X-Tenant-ID');
if (tenantHeader && await isValidTenant(tenantHeader)) {
return tenantHeader;
}
// Method 3: JWT claim
const token = await extractToken(request);
return token.tenant_id;
}
Tenant-Aware Services
class TenantAwareService:
def __init__(self, repository, cache, tenant_id):
self.repository = repository
self.cache = cache
self.tenant_id = tenant_id
def get_order(self, order_id):
cache_key = f"{self.tenant_id}:order:{order_id}"
# Check cache
cached = self.cache.get(cache_key)
if cached:
return cached
# Fetch from database
order = self.repository.get(order_id, self.tenant_id)
# Cache with tenant-specific key
self.cache.set(cache_key, order)
return order
Feature Flagging per Tenant
Enable features conditionally:
class FeatureService {
private tenantConfig: Map<string, TenantConfig>;
isEnabled(tenantId: string, feature: string): boolean {
const config = this.tenantConfig.get(tenantId);
return config?.features?.[feature] ?? false;
}
// Usage in code
async handleRequest(req: Request) {
if (this.isEnabled(req.tenantId, 'beta-features')) {
return this.handleBetaRequest(req);
}
return this.handleStandardRequest(req);
}
}
Scaling Strategies
Horizontal Scaling with Tenant Affinity
# Kubernetes: Tenant-aware pod placement
apiVersion: v1
kind: Pod
metadata:
labels:
app: api-server
tenant: tenant-a
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
tenant: tenant-a
topologyKey: kubernetes.io/hostname
Connection Pooling
Manage database connections per tenant:
# Tenant-aware connection pool
class TenantConnectionPool:
def __init__(self, base_pool):
self.base_pool = base_pool
self.tenant_pools = {}
def get_connection(self, tenant_id):
if tenant_id not in self.tenant_pools:
# Create dedicated pool for high-volume tenants
if self.is_high_volume(tenant_id):
self.tenant_pools[tenant_id] = self.create_pool(
min_size=10, max_size=50
)
else:
# Share base pool for low-volume tenants
self.tenant_pools[tenant_id] = self.base_pool
return self.tenant_pools[tenant_id].get_connection()
Resource Quotas
# Kubernetes ResourceQuota per tenant
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-a-quota
namespace: tenant-a
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
pods: "50"
services: "10"
Security Considerations
Data Isolation Verification
# Tenant isolation testing
class TenantIsolationTest:
async def test_data_leakage(self):
# Create data for tenant A
tenant_a_token = self.generate_token(tenant_id='tenant-a')
await self.create_order(tenant_a_token, {'item': 'secret-a'})
# Try to access from tenant B
tenant_b_token = self.generate_token(tenant_id='tenant-b')
result = await self.get_orders(tenant_b_token)
# Verify no leakage
assert 'secret-a' not in result
assert len(result) == 0 # Tenant B has no orders
Audit Logging
# Comprehensive audit trail
class AuditLogger:
def log_access(self, tenant_id, user_id, resource, action):
self.logger.info({
'timestamp': datetime.utcnow().isoformat(),
'tenant_id': tenant_id,
'user_id': user_id,
'resource': resource,
'action': action,
'ip_address': self.get_client_ip(),
'user_agent': self.get_user_agent()
})
Migration Strategies
Upgrading Multi-Tenant Systems
- Blue-green tenant migration: Migrate tenants in batches
- Feature flags for compatibility: Toggle old/new behavior
- Backward compatibility window: Support both versions
# Phased migration
async def migrate_tenant(tenant_id, batch_size=100):
# Phase 1: Replicate data
await replicate_data(tenant_id)
# Phase 2: Validate consistency
await validate_consistency(tenant_id)
# Phase 3: Switch traffic
await switch_traffic(tenant_id)
# Phase 4: Cleanup old data
await cleanup_old_data(tenant_id)
Best Practices
- Design for tenant isolation from day one
- Use tenant ID in all database queries
- Implement tenant-aware caching
- Monitor per-tenant resource usage
- Plan for tenant-specific customization
- Automate tenant provisioning
- Test isolation boundaries regularly
Resources
Conclusion
Multi-tenant architecture is fundamental to building successful SaaS applications. The choice of tenancy model depends on isolation requirements, cost constraints, and operational capabilities. Start with shared-everything and evolve toward stronger isolation as needed. The key is designing for tenant isolation from the beginning while maintaining operational efficiency.
Comments