System Design, Scalability, and Capstone
Synthesize knowledge to design, implement, and present a production-ready full stack project.
Content
Scalability Strategies
Versions:
Watch & Learn
AI-discovered learning video
Scalability Strategies: Scale Like You Mean It (Without Setting Your Wallet on Fire)
"Congrats, your app went viral. Also congrats, your database is now a single point of failure with performance anxiety."
We’ve already picked our storage weapons (Storage Choices) and made our APIs spell out their intentions (API Contracts). Plus, we tuned performance and wired up observability like responsible adults. Now it’s time for the spicier sequel: how to scale when people actually show up.
Scalability is not just “make it faster.” It’s “handle more without falling apart,” while keeping latency, cost, and complexity in a stable, mutually suspicious relationship.
What We’re Optimizing For (a.k.a. Metrics That Decide If You Sleep Tonight)
- Throughput: requests per second (RPS/QPS)
- Latency: especially tail latencies (p95/p99)
- Elasticity: how quickly you can add/remove capacity
- Cost per request: your CFO’s favorite SLO
- Error budgets & SLOs: from our Monitoring unit — define target p95 and error rate; scale to maintain them, not vibes
Imagine this funnel:
Client -> CDN/Edge Cache -> Load Balancer -> App Tier (stateless) -> Cache -> DB/Queue -> Async Workers -> Storage(s)
Every arrow is a chance to reduce load, hide latency, or summon chaos. Choose wisely.
Strategy Zero: Scale-Up vs Scale-Out (and Why Statelessness Is Your BFF)
- Vertical scaling (scale-up): Bigger box. Easy, fast, $$$ ceiling.
- Horizontal scaling (scale-out): More boxes. Requires stateless services and shared-nothing mindset.
Key moves:
- Stateless services: Store session in cookies/JWTs or a shared store (Redis), not in instance memory.
- Sticky sessions (meh): Sometimes needed short-term; better to fix statefulness long-term.
- Health checks + autoscaling: Use p95 latency, CPU, and queue backlog from monitoring to drive scale decisions.
Rule: If your API contract requires multiple calls to build one screen, you built a scaling tax. Batch it.
Read-Heavy Hacks: Caching and Friends
You don’t need a bigger database if you can stop bothering it.
Caching Tiers
- Client/CDN/Edge cache: Best for static or public content; set Cache-Control headers.
- Service cache (Redis/Memcached): Hot objects, computed results, session tokens.
- DB cache: Sometimes built-in (e.g., Postgres buffer cache); don’t fight it, complement it.
Common pitfalls and power-ups:
- Invalidation: Use TTLs, cache-aside (read-through) patterns, or write-through on updates.
- Stampede (thundering herd): Coalesce duplicate requests and add jitter.
# single-flight style cache fetch
function get(key):
if cache.has(key): return cache.get(key)
lock = inflight.get_or_create(key)
with lock:
if cache.has(key): return cache.get(key) # maybe another request filled it
val = db.query(key)
cache.set(key, val, ttl=60 + rand(0,10))
return val
Read Replicas
- Replicate primary DB to multiple read-only replicas.
- Use for read-heavy endpoints, analytics, search.
- Trade-offs: replication lag (eventual reads), operational complexity.
Tie-back to Storage Choices: If you picked a NoSQL store with tunable consistency, use relaxed reads for browse flows.
Write-Heavy Moves: Partition Like a Pro
When writes crank up, one box will cry.
Sharding/Partitioning
- Hash-based: Even distribution, terrible for range queries.
- Range-based: Great for time-series and range scans, risks hotspots.
- Geo-based: Keeps data near users, raises cross-region consistency headaches.
Hotspot bingo:
- Auto-increment IDs => writes stack on one shard. Fix with ULIDs/Snowflake IDs or hash of entity ID.
- Viral entity (one user/product) => route by userID, not timestamp.
CQRS and Append-Only Logs
- CQRS: Separate write model from read model. Writes go to the source of truth; reads are precomputed, denormalized views.
- Event sourcing/append-only: Durable audit trail, easy rebuilds, eventual consistency on views.
Remember CAP from Storage Choices: as you partition and replicate, you’re choosing where to be consistent and where to be available.
Asynchrony: Queues, Backpressure, and Not Making Users Wait
Not everything needs to happen in the request thread.
- Queue offloads: Email, image processing, payments confirmation, search indexing.
- Workers consume at steady rate: Smooth traffic spikes, reduce tail latency.
- Delivery semantics: at-least-once is common; make handlers idempotent.
POST /payments
Idempotency-Key: 5a7d-9b21-...
Body: { "amount": 1999, "currency": "USD" }
Server promise (from API Contracts): same Idempotency-Key => same outcome.
Retry sanely:
function retry(op):
for i in range(5):
try: return op()
except TransientError:
sleep = min(100ms * 2^i, 5s) + rand(0,100ms) # exponential backoff + jitter
wait(sleep)
raise
Traffic Management: Load Balancers, Rate Limits, and Circuit Breakers
- Load balancer: Spreads load, terminates TLS, health checks.
- Rate limiting: Protects your core. Token bucket is the chill cousin of DoS defenses.
- Backpressure: 429 Too Many Requests + Retry-After; don’t just suffer, push back.
- Circuit breakers: If a dependency is flaking, fail fast or return cached/stale results.
- Bulkheads: Isolate resources so one failing feature doesn’t sink the ship.
# token bucket
bucket = { capacity: 100, tokens: 100, refill: 50/sec }
if bucket.tokens > 0: bucket.tokens--, allow()
else: reject(429)
Deployment scaling synergy: pair with canary and blue/green so you scale bad code to fewer users first.
API Contracts That Scale Nicely (You Did This Unit Already, Now Flex It)
- Pagination: Prefer cursor-based over offset for consistency and speed.
- Batching:
POST /batch/users> 1000 chattyGET /user/{id}. - Idempotency keys: Mandatory for at-least-once retries.
- Versioning: Add fields, don’t break clients; deprecate with runway.
- Rate limit headers:
X-RateLimit-Remaining,Retry-Afterto guide client behavior.
If your contract forces N+1 calls, you’ve pre-booked a p99 nightmare.
Multi-Region and Edge: The Spicy Trade-Offs
- CDN + edge functions: Push reads and simple logic closer to users; great for public content and cacheable APIs.
- Leader/follower: Single write region, many read regions. Lowest complexity, higher write latency for far users.
- Active-active: Write anywhere, then resolve conflicts (CRDTs, vector clocks, last-write-wins if you like chaos). Complexity tax applies.
- Write-forwarding: Accept locally, forward to primary for commit, return when durable (or async if UX allows).
Regulatory spice: data residency and compliance can dictate where data lives. Plan shards and caches accordingly.
Cost-Aware Scaling (Because Your CFO Also Reads Dashboards)
- Reduce request amplification: Aggregate backend calls; cache whole-page fragments.
- Right-size instances: Too big = expensive; too small = noisy neighbors and GC meltdowns.
- Autoscale on meaningful signals: p95 latency, queue depth, custom SLOs, not just CPU.
- Precompute hot reads: Materialized views, search indexes, denormalized tables — rebuild asynchronously.
Monitoring tie-in: alert on SLO burn rates and saturation, then scale or shed load before users feel it.
Anti-Patterns Speedrun
- N+1 queries (ORM, I’m looking at you). Fix with eager loading or joins.
- Global transactions across microservices. Use sagas/compensation, not two-phase fantasy.
- One database for everything. Split OLTP vs analytics; use read replicas, warehouses.
- Hot partitions from timestamp keys. Hash or bucket.
- Cache without invalidation strategy. You’ve built a rumor mill.
- Ignoring cold starts in serverless. Warm pools or provisioned concurrency for spiky traffic.
Strategy Cheat Sheet
| Strategy | Best For | Trade-offs | Key Knobs |
|---|---|---|---|
| CDN/Edge Cache | Static/public content | Stale data, cache keys | TTLs, cache-control |
| App Cache (Redis) | Hot reads, sessions | Invalidation, memory | TTLs, eviction, single-flight |
| Read Replicas | Read-heavy | Lag, consistency | Routing, staleness tolerance |
| Sharding | Write scale | Complexity, hotspots | Shard key, rebalancing |
| Queues/Async | Spikes, long tasks | Eventual UX, retries | DLQs, idempotency |
| CQRS | Complex reads | Duplication, sync lag | View rebuild cadence |
| Autoscaling | Elastic traffic | Thrash if noisy | Target p95, cooldowns |
| Rate Limiting | Abuse, fairness | Denied requests | Bucket size/refill |
A Tiny Decision Flow
- Is it read-heavy? Cache, replicas, denormalized views.
- Is it write-heavy? Shard smartly, queue, CQRS.
- Is latency spiking at p99? Add caches, batch API calls, scale out app tier.
- Is DB CPU high? Offload reads, add indexes, move expensive joins to precomputed views.
- Is external dependency flaky? Circuit-break + fallback.
- Is traffic spiky? Autoscale app + buffer with queues.
Wrap-Up: The Vibe Checklist
- Make services stateless and observable.
- Push reads out (CDN, cache, views), push writes across (shards, queues).
- Let your API contract carry weight: batching, pagination, idempotency, rate hints.
- Scale to SLOs, not to feelings. Cost per request matters.
Big idea: Scalability isn’t one trick — it’s choreography. Every layer does a little less, a little earlier, so nothing has to do everything at the end.
Next time your traffic graph looks like a hockey stick, don’t panic. Move work earlier (cache), later (async), elsewhere (edge, shards), or nowhere (drop/limit). That’s not giving up. That’s designing like a legend.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!