Building Scalable SaaS Applications: Best Practices
Building a SaaS application that can scale from 100 to 100,000 users requires careful architectural planning from day one. This guide covers the essential patterns and practices for creating truly scalable SaaS products.
Foundation: Multi-Tenancy Architecture
Multi-tenancy is the cornerstone of SaaS architecture. Choose the right model based on your requirements.
Single Database, Shared Schema
All tenants share tables with tenant identifiers.
-- Example: Shared schema with tenant isolation
CREATE TABLE users (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT unique_email_per_tenant UNIQUE (tenant_id, email)
);
CREATE INDEX idx_users_tenant ON users(tenant_id);
Pros: Cost-effective, simple deployment Cons: Requires careful query optimization, potential noisy neighbor issues
Single Database, Separate Schemas
Each tenant has their own schema within a shared database.
Pros: Better isolation, easier data management Cons: More complex migrations, schema proliferation
Separate Databases
Each tenant has a dedicated database.
Pros: Maximum isolation, easy compliance Cons: Higher costs, complex orchestration
Scalability Patterns
1. Horizontal Scaling with Microservices
Break your monolith into focused services:
┌─────────────────┐
│ API Gateway │
└────────┬────────┘
│
┌────┴────┐
│ │
┌───▼───┐ ┌───▼───┐
│ Auth │ │ Core │
│Service│ │Service│
└───┬───┘ └───┬───┘
│ │
┌───▼─────────▼───┐
│ Message Queue │
└─────────────────┘
2. Database Scaling Strategies
Read Replicas
- Distribute read queries across replicas
- Use connection pooling (PgBouncer, ProxySQL)
- Implement caching layers
Sharding
// Tenant-based sharding logic
const getShardForTenant = (tenantId) => {
const shardCount = 4;
const hash = hashTenantId(tenantId);
return `shard_${hash % shardCount}`;
};
3. Caching Strategy
Implement multi-level caching:
// Multi-level cache implementation
class CacheService {
async get(key) {
// L1: In-memory (fastest)
let value = this.memoryCache.get(key);
if (value) return value;
// L2: Redis (distributed)
value = await this.redis.get(key);
if (value) {
this.memoryCache.set(key, value);
return value;
}
// L3: Database (source of truth)
value = await this.database.get(key);
await this.redis.set(key, value, 'EX', 3600);
this.memoryCache.set(key, value);
return value;
}
}
Performance Optimization
API Design for Scale
Pagination
// Cursor-based pagination (performant)
GET /api/users?cursor=eyJpZCI6MTAwfQ&limit=20
// Response
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ",
"has_more": true
}
}
Rate Limiting
// Token bucket implementation
const rateLimit = {
windowMs: 60 * 1000,
max: 100,
keyGenerator: (req) => req.tenantId,
handler: (req, res) => {
res.status(429).json({ error: 'Rate limit exceeded' });
}
};
Background Job Processing
Offload heavy tasks to background workers:
// Queue configuration
const queue = new Queue('heavy-tasks', {
redis: redisConfig,
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
}
});
// Process jobs
queue.process('report-generation', async (job) => {
const { tenantId, reportType } = job.data;
await generateReport(tenantId, reportType);
});
Security at Scale
Data Isolation
// Middleware ensuring tenant isolation
const tenantIsolation = async (req, res, next) => {
const tenantId = req.user.tenantId;
// Inject tenant context into all database queries
req.db = req.db.withTenant(tenantId);
// Audit logging
await auditLog({
tenantId,
userId: req.user.id,
action: req.method,
resource: req.path
});
next();
};
Encryption
- At Rest: AES-256 encryption for stored data
- In Transit: TLS 1.3 for all communications
- Per-Tenant Keys: Separate encryption keys per tenant
Monitoring and Observability
Key Metrics to Track
# SaaS-specific metrics
metrics:
- tenant_active_users
- api_latency_p99
- error_rate_by_tenant
- database_query_time
- cache_hit_ratio
- background_job_queue_depth
- storage_usage_by_tenant
Distributed Tracing
// OpenTelemetry setup
const trace = api.trace.getTracer('saas-app');
const handleRequest = async (req, res) => {
const span = trace.startSpan('handle-request', {
attributes: {
'tenant.id': req.tenantId,
'http.method': req.method,
'http.url': req.url
}
});
try {
await processRequest(req, res);
} finally {
span.end();
}
};
Cost Optimization
Resource Allocation
// Tier-based resource limits
const tierLimits = {
free: {
maxUsers: 5,
storageGB: 1,
apiCallsPerMonth: 1000
},
professional: {
maxUsers: 50,
storageGB: 100,
apiCallsPerMonth: 100000
},
enterprise: {
maxUsers: -1, // unlimited
storageGB: 1000,
apiCallsPerMonth: -1
}
};
Usage-Based Billing
Track and bill based on actual usage:
- API calls
- Storage consumption
- Active users
- Feature usage
Deployment Strategy
Zero-Downtime Deployments
# Kubernetes rolling update
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
Feature Flags
// Feature flag implementation
const featureFlags = {
async isEnabled(flag, tenantId) {
const config = await this.getConfig(flag);
if (config.globalEnabled) return true;
if (config.tenants.includes(tenantId)) return true;
if (config.percentage > 0) {
return this.percentageRollout(tenantId, config.percentage);
}
return false;
}
};
Conclusion
Building a scalable SaaS application requires thoughtful architecture decisions from the start. Focus on multi-tenancy, horizontal scaling, security, and observability to create a product that grows with your customer base.
Building a SaaS Product?
Affor Technologies specializes in architecting and building scalable SaaS applications. Let's discuss your project and create something amazing together.
Ready to Build Your Next Project?
Let our experts help you turn your ideas into reality. Get started with a free consultation today.
Get a Free Consultation