# Scaling SaaS Infrastructure **Meta-Description:** SaaS-Infrastruktur skalieren. Von Monolith zu Microservices, Database Sharding, Caching-Strategien und Auto-Scaling mit Kubernetes implementieren. **Keywords:** SaaS Scaling, Horizontal Scaling, Database Sharding, Kubernetes, Load Balancing, Caching, CDN, Auto-Scaling, High Availability --- ## Einführung **SaaS Infrastructure Scaling** ermöglicht Wachstum ohne Performance-Einbußen. Von **horizontalem Scaling** über **Database Sharding** bis **Kubernetes Auto-Scaling** – die richtige Architektur entscheidet über Skalierbarkeit. Dieser Guide zeigt bewährte Patterns für wachsende SaaS-Produkte. --- ## Scaling Architecture Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ SAAS SCALING ARCHITECTURE │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ CDN LAYER │ │ │ │ CloudFlare / AWS CloudFront / Vercel Edge │ │ │ │ ├── Static assets (JS, CSS, images) │ │ │ │ ├── Edge caching for API responses │ │ │ │ └── DDoS protection │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ LOAD BALANCER │ │ │ │ AWS ALB / Nginx / Traefik │ │ │ │ ├── SSL termination │ │ │ │ ├── Health checks │ │ │ │ ├── Rate limiting │ │ │ │ └── Geographic routing │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ┌────────────────┼────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ App Pod │ │ App Pod │ │ App Pod │ │ │ │ (API) │ │ (API) │ │ (API) │ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │ │ │ │ │ └────────────────┼────────────────┘ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ CACHE LAYER │ │ │ │ Redis Cluster / Memcached │ │ │ │ ├── Session storage │ │ │ │ ├── Query caching │ │ │ │ ├── Rate limit counters │ │ │ │ └── Real-time features (pub/sub) │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ DATABASE LAYER │ │ │ │ │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ Primary │──│ Replica │ │ │ │ │ │ (Write) │ │ (Read) │ │ │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ │ │ ▼ (at scale) │ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ │ │ SHARDED DATABASE │ │ │ │ │ │ Shard 1 │ Shard 2 │ Shard 3 │ Shard N │ │ │ │ │ │ (A-G) │ (H-N) │ (O-T) │ (U-Z) │ │ │ │ │ └──────────────────────────────────────────┘ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Background Jobs: Redis Queue / BullMQ / SQS │ │ Search: Elasticsearch / Meilisearch / Algolia │ │ Storage: S3 / R2 / GCS │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Scaling Stages ``` ┌─────────────────────────────────────────────────────────────┐ │ SAAS SCALING JOURNEY │ ├─────────────────────────────────────────────────────────────┤ │ │ │ STAGE 1: Single Server (0-1K users) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ App + DB on one server │ │ │ │ ✓ Simple deployment │ │ │ │ ✗ Single point of failure │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ STAGE 2: Separate DB (1K-10K users) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ App Server ──► Managed Database (RDS, PlanetScale) │ │ │ │ ✓ Independent scaling │ │ │ │ ✓ Automatic backups │ │ │ │ + Add Redis for sessions/caching │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ STAGE 3: Horizontal Scaling (10K-100K users) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Load Balancer ──► Multiple App Instances │ │ │ │ ──► Read Replicas │ │ │ │ ✓ No single point of failure │ │ │ │ ✓ Zero-downtime deployments │ │ │ │ + CDN for static assets │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ STAGE 4: Microservices (100K+ users) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ API Gateway ──► Service Mesh │ │ │ │ ──► Kubernetes │ │ │ │ ──► Database per service │ │ │ │ ✓ Independent deployment & scaling │ │ │ │ ✓ Technology flexibility │ │ │ │ + Database sharding │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Database Read Replicas ```typescript // lib/db/replica.ts import { PrismaClient } from '@prisma/client'; // Primary for writes export const primaryDb = new PrismaClient({ datasources: { db: { url: process.env.DATABASE_URL } } }); // Replica for reads export const replicaDb = new PrismaClient({ datasources: { db: { url: process.env.DATABASE_REPLICA_URL } } }); // Smart routing interface DbOptions { write?: boolean; } export function getDb(options: DbOptions = {}): PrismaClient { return options.write ? primaryDb : replicaDb; } // Usage examples export async function getUsers() { // Read from replica return getDb().user.findMany(); } export async function createUser(data: UserCreateInput) { // Write to primary return getDb({ write: true }).user.create({ data }); } // Read-after-write consistency helper export async function createUserWithRead(data: UserCreateInput) { const user = await primaryDb.user.create({ data }); // For immediate read-after-write, use primary // Replica lag is typically <100ms return user; } ``` --- ## Prisma with Read Replicas Extension ```typescript // lib/db/prisma-replicas.ts import { PrismaClient } from '@prisma/client'; import { readReplicas } from '@prisma/extension-read-replicas'; export const db = new PrismaClient().$extends( readReplicas({ url: process.env.DATABASE_REPLICA_URL!, // Optional: multiple replicas for load distribution // url: [ // process.env.DATABASE_REPLICA_URL_1!, // process.env.DATABASE_REPLICA_URL_2! // ] }) ); // All reads automatically go to replica // All writes automatically go to primary // Force primary for specific reads (read-after-write) export async function getUserAfterUpdate(userId: string) { return db.$primary().user.findUnique({ where: { id: userId } }); } ``` --- ## Caching Strategy ```typescript // lib/cache/redis.ts import Redis from 'ioredis'; const redis = new Redis(process.env.REDIS_URL!); interface CacheOptions { ttl?: number; // seconds tags?: string[]; } export async function cache( key: string, fetcher: () => Promise, options: CacheOptions = {} ): Promise { const { ttl = 3600 } = options; // Try cache first const cached = await redis.get(key); if (cached) { return JSON.parse(cached); } // Fetch fresh data const data = await fetcher(); // Store in cache await redis.setex(key, ttl, JSON.stringify(data)); // Track tags for invalidation if (options.tags) { for (const tag of options.tags) { await redis.sadd(`tag:${tag}`, key); } } return data; } export async function invalidateByTag(tag: string): Promise { const keys = await redis.smembers(`tag:${tag}`); if (keys.length > 0) { await redis.del(...keys); await redis.del(`tag:${tag}`); } } // Cache-aside pattern for database queries export async function cachedQuery( queryKey: string, query: () => Promise, ttl: number = 300 ): Promise { return cache(queryKey, query, { ttl }); } // Usage export async function getProjectsByTenant(tenantId: string) { return cachedQuery( `tenant:${tenantId}:projects`, () => db.project.findMany({ where: { tenantId }, orderBy: { updatedAt: 'desc' } }), 600 // 10 minutes ); } ``` --- ## Multi-Level Caching ```typescript // lib/cache/multi-level.ts import { LRUCache } from 'lru-cache'; import Redis from 'ioredis'; const redis = new Redis(process.env.REDIS_URL!); // L1: In-memory cache (per instance) const memoryCache = new LRUCache({ max: 1000, ttl: 60 * 1000 // 1 minute }); // L2: Redis (shared) interface MultiLevelCacheOptions { memoryTtl?: number; // ms redisTtl?: number; // seconds } export async function multiLevelCache( key: string, fetcher: () => Promise, options: MultiLevelCacheOptions = {} ): Promise { const { memoryTtl = 60_000, redisTtl = 300 } = options; // L1: Check memory const memoryHit = memoryCache.get(key); if (memoryHit !== undefined) { return memoryHit as T; } // L2: Check Redis const redisHit = await redis.get(key); if (redisHit) { const data = JSON.parse(redisHit); memoryCache.set(key, data, { ttl: memoryTtl }); return data; } // L3: Fetch from source const data = await fetcher(); // Populate both caches memoryCache.set(key, data, { ttl: memoryTtl }); await redis.setex(key, redisTtl, JSON.stringify(data)); return data; } // Cache invalidation across instances via pub/sub const subscriber = new Redis(process.env.REDIS_URL!); subscriber.subscribe('cache:invalidate'); subscriber.on('message', (channel, key) => { if (channel === 'cache:invalidate') { memoryCache.delete(key); } }); export async function invalidate(key: string): Promise { memoryCache.delete(key); await redis.del(key); // Notify other instances await redis.publish('cache:invalidate', key); } ``` --- ## Database Connection Pooling ```typescript // lib/db/pool.ts import { Pool, PoolConfig } from 'pg'; const poolConfig: PoolConfig = { connectionString: process.env.DATABASE_URL, // Pool sizing min: 2, max: 20, // max connections per instance // Timeouts connectionTimeoutMillis: 5000, idleTimeoutMillis: 30000, // Query timeout statement_timeout: 30000, // Application name for monitoring application_name: 'saas-app' }; export const pool = new Pool(poolConfig); // Health check export async function checkDbHealth(): Promise { try { const result = await pool.query('SELECT 1'); return result.rows.length > 0; } catch { return false; } } // Connection pool monitoring pool.on('connect', () => { console.log('New connection established'); }); pool.on('error', (err) => { console.error('Unexpected error on idle client', err); }); // Graceful shutdown process.on('SIGTERM', async () => { await pool.end(); }); ``` --- ## Database Sharding ```typescript // lib/db/sharding.ts import { PrismaClient } from '@prisma/client'; import crypto from 'crypto'; interface ShardConfig { id: number; url: string; range: [number, number]; // Hash range } const SHARDS: ShardConfig[] = [ { id: 0, url: process.env.SHARD_0_URL!, range: [0, 63] }, { id: 1, url: process.env.SHARD_1_URL!, range: [64, 127] }, { id: 2, url: process.env.SHARD_2_URL!, range: [128, 191] }, { id: 3, url: process.env.SHARD_3_URL!, range: [192, 255] } ]; // Create client for each shard const shardClients = new Map(); for (const shard of SHARDS) { shardClients.set( shard.id, new PrismaClient({ datasources: { db: { url: shard.url } } }) ); } // Consistent hashing for shard selection export function getShardId(tenantId: string): number { const hash = crypto .createHash('md5') .update(tenantId) .digest()[0]; // First byte (0-255) const shard = SHARDS.find( s => hash >= s.range[0] && hash <= s.range[1] ); return shard?.id || 0; } export function getShardClient(tenantId: string): PrismaClient { const shardId = getShardId(tenantId); return shardClients.get(shardId)!; } // Usage export async function getProjectsForTenant(tenantId: string) { const client = getShardClient(tenantId); return client.project.findMany({ where: { tenantId } }); } // Cross-shard queries (avoid if possible) export async function globalSearch(query: string) { const results = await Promise.all( Array.from(shardClients.values()).map(client => client.project.findMany({ where: { OR: [ { name: { contains: query, mode: 'insensitive' } }, { description: { contains: query, mode: 'insensitive' } } ] }, take: 10 }) ) ); return results.flat().slice(0, 50); } ``` --- ## Background Job Processing ```typescript // lib/jobs/queue.ts import { Queue, Worker, Job } from 'bullmq'; import Redis from 'ioredis'; const connection = new Redis(process.env.REDIS_URL!, { maxRetriesPerRequest: null }); // Define queues export const emailQueue = new Queue('email', { connection }); export const reportQueue = new Queue('reports', { connection }); export const webhookQueue = new Queue('webhooks', { connection }); // Email worker const emailWorker = new Worker( 'email', async (job: Job) => { const { to, template, data } = job.data; await sendEmail(to, template, data); }, { connection, concurrency: 10, limiter: { max: 100, duration: 1000 // 100 emails per second } } ); // Report worker (CPU-intensive) const reportWorker = new Worker( 'reports', async (job: Job) => { const { tenantId, reportType, dateRange } = job.data; return generateReport(tenantId, reportType, dateRange); }, { connection, concurrency: 2, // Lower concurrency for heavy jobs lockDuration: 300000 // 5 minute lock } ); // Webhook worker with retry const webhookWorker = new Worker( 'webhooks', async (job: Job) => { const { url, payload, signature } = job.data; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Signature': signature }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`Webhook failed: ${response.status}`); } }, { connection, concurrency: 20, defaultJobOptions: { attempts: 5, backoff: { type: 'exponential', delay: 1000 // 1s, 2s, 4s, 8s, 16s } } } ); // Add jobs export async function queueEmail( to: string, template: string, data: Record ) { return emailQueue.add('send', { to, template, data }); } export async function queueReport( tenantId: string, reportType: string, dateRange: { start: Date; end: Date } ) { return reportQueue.add( 'generate', { tenantId, reportType, dateRange }, { priority: 10 } ); } // Scheduled jobs export async function setupScheduledJobs() { // Daily cleanup await emailQueue.add( 'cleanup', {}, { repeat: { cron: '0 2 * * *' } // 2 AM daily } ); // Hourly metrics await reportQueue.add( 'metrics', {}, { repeat: { cron: '0 * * * *' } // Every hour } ); } ``` --- ## Kubernetes Auto-Scaling ```yaml # k8s/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: saas-api spec: replicas: 3 selector: matchLabels: app: saas-api template: metadata: labels: app: saas-api spec: containers: - name: api image: saas-api:latest ports: - containerPort: 3000 resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" env: - name: NODE_ENV value: "production" - name: DATABASE_URL valueFrom: secretKeyRef: name: db-secrets key: url livenessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 3000 initialDelaySeconds: 5 periodSeconds: 5 --- # Horizontal Pod Autoscaler apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: saas-api-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: saas-api minReplicas: 3 maxReplicas: 50 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 10 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 15 - type: Pods value: 4 periodSeconds: 15 selectPolicy: Max --- # Pod Disruption Budget apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: saas-api-pdb spec: minAvailable: 2 selector: matchLabels: app: saas-api ``` --- ## Health Check Endpoints ```typescript // app/api/health/route.ts import { NextResponse } from 'next/server'; import { db } from '@/lib/db'; import Redis from 'ioredis'; const redis = new Redis(process.env.REDIS_URL!); // Liveness probe - is the app running? export async function GET() { return NextResponse.json({ status: 'ok' }); } // app/api/ready/route.ts // Readiness probe - can the app handle traffic? export async function GET() { const checks = await Promise.allSettled([ checkDatabase(), checkRedis(), checkExternalServices() ]); const results = { database: checks[0].status === 'fulfilled', redis: checks[1].status === 'fulfilled', external: checks[2].status === 'fulfilled' }; const allHealthy = Object.values(results).every(Boolean); return NextResponse.json( { status: allHealthy ? 'ready' : 'degraded', checks: results }, { status: allHealthy ? 200 : 503 } ); } async function checkDatabase(): Promise { try { await db.$queryRaw`SELECT 1`; return true; } catch { return false; } } async function checkRedis(): Promise { try { await redis.ping(); return true; } catch { return false; } } async function checkExternalServices(): Promise { // Check critical external dependencies try { const response = await fetch('https://api.stripe.com/v1/health', { method: 'HEAD', signal: AbortSignal.timeout(5000) }); return response.ok; } catch { return true; // Don't fail readiness for optional services } } ``` --- ## Rate Limiting at Scale ```typescript // lib/rate-limit/distributed.ts import Redis from 'ioredis'; const redis = new Redis(process.env.REDIS_URL!); interface RateLimitConfig { windowMs: number; maxRequests: number; } const TIERS: Record = { free: { windowMs: 60000, maxRequests: 60 }, // 60/min pro: { windowMs: 60000, maxRequests: 600 }, // 600/min enterprise: { windowMs: 60000, maxRequests: 6000 } // 6000/min }; interface RateLimitResult { allowed: boolean; remaining: number; resetAt: number; } export async function checkRateLimit( identifier: string, tier: string = 'free' ): Promise { const config = TIERS[tier] || TIERS.free; const key = `ratelimit:${identifier}`; const now = Date.now(); const windowStart = now - config.windowMs; // Sliding window using sorted set const pipeline = redis.pipeline(); // Remove old entries pipeline.zremrangebyscore(key, 0, windowStart); // Count current window pipeline.zcard(key); // Add current request pipeline.zadd(key, now.toString(), `${now}:${Math.random()}`); // Set expiry pipeline.pexpire(key, config.windowMs); const results = await pipeline.exec(); const currentCount = (results?.[1]?.[1] as number) || 0; const allowed = currentCount < config.maxRequests; const remaining = Math.max(0, config.maxRequests - currentCount - 1); const resetAt = now + config.windowMs; return { allowed, remaining, resetAt }; } // Middleware export async function rateLimitMiddleware( request: Request, identifier: string, tier: string ): Promise { const result = await checkRateLimit(identifier, tier); if (!result.allowed) { return new Response( JSON.stringify({ error: 'Rate limit exceeded' }), { status: 429, headers: { 'Content-Type': 'application/json', 'X-RateLimit-Limit': TIERS[tier]?.maxRequests.toString() || '60', 'X-RateLimit-Remaining': '0', 'X-RateLimit-Reset': result.resetAt.toString(), 'Retry-After': Math.ceil((result.resetAt - Date.now()) / 1000).toString() } } ); } return null; // Allow request } ``` --- ## CDN & Edge Caching ```typescript // next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { // Static assets images: { remotePatterns: [ { protocol: 'https', hostname: 'cdn.example.com' } ], minimumCacheTTL: 60 * 60 * 24 * 30 // 30 days }, // Cache headers async headers() { return [ { source: '/api/:path*', headers: [ { key: 'Cache-Control', value: 'no-store, must-revalidate' } ] }, { source: '/_next/static/:path*', headers: [ { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' } ] }, { source: '/images/:path*', headers: [ { key: 'Cache-Control', value: 'public, max-age=86400, stale-while-revalidate=604800' } ] } ]; } }; module.exports = nextConfig; // Edge-cached API response // app/api/products/route.ts import { NextResponse } from 'next/server'; export const runtime = 'edge'; export const revalidate = 60; // ISR: revalidate every 60 seconds export async function GET() { const products = await fetchProducts(); return NextResponse.json(products, { headers: { 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300' } }); } ``` --- ## Monitoring & Observability ```typescript // lib/monitoring/metrics.ts import { Counter, Histogram, Registry } from 'prom-client'; export const registry = new Registry(); // Request metrics export const httpRequestsTotal = new Counter({ name: 'http_requests_total', help: 'Total HTTP requests', labelNames: ['method', 'path', 'status'], registers: [registry] }); export const httpRequestDuration = new Histogram({ name: 'http_request_duration_seconds', help: 'HTTP request duration', labelNames: ['method', 'path'], buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5], registers: [registry] }); // Database metrics export const dbQueryDuration = new Histogram({ name: 'db_query_duration_seconds', help: 'Database query duration', labelNames: ['operation', 'table'], buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1], registers: [registry] }); // Business metrics export const activeUsers = new Counter({ name: 'active_users_total', help: 'Total active users', labelNames: ['tier'], registers: [registry] }); // Prometheus endpoint // app/api/metrics/route.ts import { registry } from '@/lib/monitoring/metrics'; export async function GET() { const metrics = await registry.metrics(); return new Response(metrics, { headers: { 'Content-Type': registry.contentType } }); } ``` --- ## Graceful Degradation ```typescript // lib/resilience/circuit-breaker.ts interface CircuitBreakerOptions { failureThreshold: number; resetTimeout: number; } type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN'; export class CircuitBreaker { private state: CircuitState = 'CLOSED'; private failureCount = 0; private lastFailureTime?: number; constructor(private options: CircuitBreakerOptions) {} async execute(fn: () => Promise, fallback?: () => T): Promise { if (this.state === 'OPEN') { if (this.shouldAttemptReset()) { this.state = 'HALF_OPEN'; } else if (fallback) { return fallback(); } else { throw new Error('Circuit breaker is OPEN'); } } try { const result = await fn(); this.onSuccess(); return result; } catch (error) { this.onFailure(); if (fallback) { return fallback(); } throw error; } } private onSuccess(): void { this.failureCount = 0; this.state = 'CLOSED'; } private onFailure(): void { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.options.failureThreshold) { this.state = 'OPEN'; } } private shouldAttemptReset(): boolean { return ( this.lastFailureTime !== undefined && Date.now() - this.lastFailureTime >= this.options.resetTimeout ); } } // Usage const searchCircuit = new CircuitBreaker({ failureThreshold: 5, resetTimeout: 30000 // 30 seconds }); export async function searchProducts(query: string) { return searchCircuit.execute( () => elasticSearch.search(query), () => db.product.findMany({ where: { name: { contains: query } }, take: 20 }) ); } ``` --- ## Best Practices | Aspect | Recommendation | |--------|----------------| | **Caching** | Multi-level: Memory → Redis → DB | | **Database** | Read replicas, then sharding | | **Stateless** | Session in Redis, not in memory | | **Jobs** | Async for anything >100ms | | **Monitoring** | Metrics, logs, traces | | **Graceful** | Circuit breakers, fallbacks | --- ## Fazit SaaS Infrastructure Scaling erfordert: 1. **Horizontal First**: Stateless Apps, Load Balancing 2. **Caching**: Multi-Level, Invalidation-Strategien 3. **Database**: Replicas, Pooling, später Sharding 4. **Resilience**: Circuit Breaker, Fallbacks 5. **Observability**: Metrics, Logging, Alerting Skalierung ist ein kontinuierlicher Prozess – immer den nächsten Engpass addressieren. --- ## Bildprompts 1. "SaaS scaling architecture diagram, multiple layers visualization" 2. "Database sharding illustration, data distribution across nodes" 3. "Kubernetes auto-scaling dashboard, pod metrics and scaling events" --- ## Quellen - [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/) - [Kubernetes Horizontal Pod Autoscaler](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) - [Redis Caching Patterns](https://redis.io/docs/manual/patterns/) - [High Scalability Blog](http://highscalability.com/) - [PlanetScale Database Scaling](https://planetscale.com/docs)