Data at Scale
Day 1Partitioning Deep-Dive
Partitioning is how you scale writes. Vertical scaling hits a ceiling and read replicas don't fix write bottlenecks. Partitioning (sharding) splits data across machines, each owning a subset, so storage, throughput and memory add up.
Hash partitioning. hash(key) % N spreads keys evenly with an O(1) lookup, but range queries become scatter-gather across every partition, and changing N moves most keys.
Range partitioning. Contiguous key ranges keep a range query on one partition, but auto-increment IDs or timestamps send every new write to the last partition.
Directory partitioning. A lookup table maps keys to partitions: any key can go anywhere and rebalancing is a directory update, but the directory is a single point of failure and adds a lookup to every request.
Consistent hashing. Keys and nodes sit on a ring; a key belongs to the first node clockwise. Adding a node moves only about 1/(N+1) of keys, from one neighbour. Virtual nodes even out the load.
hash % N| Hash | Range | |
|---|---|---|
| Distribution | Even | Skewed if keys aren't uniform |
| Range queries | Scatter-gather | One partition |
| Hot spot | One viral key | The newest range |
| Resharding | Modulo moves most keys | Split or merge a range |
- Partitioning by
hash(user_id + timestamp): perfect spread, but no more queries by user_id alone. - Range partitions on auto-increment IDs or time, so every new write lands on one partition.
- Over-partitioning early: 1000 partitions × 100 app servers = 100,000 connections.
Day 2Replication Trade-offs
Three reasons to replicate. Replicas buy high availability (fail over to a copy), read scaling (reads spread across copies; writes don't) and geographic latency (read from a nearby copy).
Sync, async, semi-sync. Sync waits for followers: no loss, slower writes. Async acks at once: fast, but a dead leader loses unreplicated writes. Semi-sync waits for one follower: the production middle ground.
Quorums. With N replicas, if W + R > N every read overlaps the latest write. N=3 with W=2, R=2 is strongly consistent; W=1, R=1 is faster but only eventual.
Replication lag anomalies. Lagging followers break read-your-writes (your update vanishes), monotonic reads (data goes back in time) and consistent prefix (a reply shows before its message).
Split brain needs fencing. A network partition can leave two leaders accepting writes. Fence the old one: STONITH, storage or network fencing, or a lease it must keep renewing in ZooKeeper or etcd.
| Leader-follower | Multi-leader | Leaderless | |
|---|---|---|---|
| Writes go to | One leader | Any regional leader | W nodes |
| Conflicts | None | LWW, merge, app logic | Timestamps, read repair |
| Breaks on | Leader death, lag | Conflict resolution | Clock skew, tail latency |
- Serving a balance, inventory count or permission check from an async replica.
- Failing over without fencing the old primary: two primaries, conflicting data.
- Last-write-wins across leaders: clocks disagree and writes silently vanish.
Day 3Rate Limiting at Scale
Limits must be counted together. With 50 servers each seeing a slice of a user's traffic, uncoordinated local counters let 100 requests through a 10-per-minute limit. Counts must be shared, synced or sticky.
Fixed window's boundary burst. One counter per window is simple and O(1), but a user can spend a full quota just before and just after the boundary: 2x the limit within seconds.
Sliding window counter. Weight the previous window by its overlap and add the current one: (8 × 75%) + 3 = 9. Two counters per key, no boundary burst; the default for API limits.
Token bucket allows controlled bursts. Tokens refill at a fixed rate up to a capacity and each request spends one: the long-term rate is bounded, bursts up to capacity pass. Compute refill from elapsed time.
Fail open or fail closed. When the limit store dies, over-allow user-facing and paid APIs; under-allow DDoS protection, cost control and login. A conservative local fallback sits in between.
| Central Redis | Local + sync | Sticky sessions | |
|---|---|---|---|
| Accuracy | High | Approximate | High per server |
| Latency | +1–2 ms | Very low | Very low |
| On failure | Total failure if Redis is down | Continues on local limits | Failover disruption |
- Each server counting alone: users spread requests and multiply the limit.
- One blanket fail-open rule: an AI call costing $0.01 each should fail closed.
- Leaky bucket for user-facing limits: it queues requests and adds latency even with spare capacity.
Day 4Hot Keys and Skew
Traffic follows Zipf's law. The nth most popular key gets about 1/n of the top key's traffic. With 1,000 keys the top 1% take ~40%; viral content pushes the top 0.01% past 90%.
Detect with a Count-Min Sketch. Fixed memory, O(depth) per update: hash the key into several counters and read the minimum. It may overestimate but never underestimates, which is all hot key detection needs.
Layer the defenses for hot reads. CDN edge, then local app memory, then Redis with split keys and request coalescing; the database sees only misses. In the lesson, traffic past the edge drops from 10,000 to ~2,800 req/s.
Write-heavy hot keys can't be cached. Buffer increments and flush periodically, shard the counter across N keys (reads sum all N), or use a G-Counter CRDT where each node owns its own count.
Bifurcate by popularity. Celebrities get their own code path: posts from accounts under 10K followers are pushed into feeds; celebrity posts are pulled at read time and merged. Show approximate counts like "1.2M likes".
| Best for | Consistency | Write cost | |
|---|---|---|---|
| Caching | Read-heavy, stale OK | Eventual | None |
| Key splitting | Read-heavy, rare writes | Weak | High fan-out |
| Local cache | Ultra-low latency | Weak | Invalidation |
| Coalescing | Misses, cold starts | Strong | None |
- Assuming uniform traffic: one viral key turns a partition into a single-node bottleneck.
- Letting a hot key expire all at once: thousands of requests stampede the database.
- Splitting keys that are written often: every write fans out to all splits.
Day 5Session Store Design
Sessions are read-heavy and forgiving. Every request validates a session (80% reads), so latency must stay under 5 ms P99. Durability is best effort: a lost session means logging in again, inconvenient but not catastrophic.
Sticky vs distributed sessions. Sticky sessions keep state in one server's memory: fastest, but a server failure logs its users out and deploys lose sessions. A distributed store (Redis) costs a network hop and survives both.
Client-side sessions can't be revoked. A JWT or encrypted cookie needs no storage and scales anywhere, but you can't invalidate it before expiry, it's capped at 4 KB and rides on every request.
Region-affine sessions. Encode the home region in the session ID (sess_east_abc123) and route there. If a region dies, its users re-authenticate; make that painless with SSO or remember-me.
Session security basics. 256-bit random IDs, a fresh ID on login to block fixation, and cookies set HttpOnly, Secure and SameSite=Lax.
| Redis Cluster | DynamoDB | |
|---|---|---|
| Latency | Sub-ms (0.2–0.5 ms) | 5–10 ms |
| Durability | Memory-only by default | Replicated across AZs |
| Operations | You run the cluster | Fully managed |
| Multi-region writes | Extra tooling | Global Tables |
- Local session caches without short TTLs or pub/sub invalidation: logged-out sessions keep working.
- A
user_sessionsset without the sessions' TTL: it fills with expired IDs. - Sequential, guessable session IDs instead of secure random ones.
BossDesign a URL Shortener at Scale
Clarify, then estimate. Ask before designing: spikes, code alphabet, duplicate URLs, analytics freshness, API rate limits. The answers set the target: 120,000 redirects/s during viral spikes.
Pre-allocated key ranges. A key service hands each API server a range of 10,000 counter values, scrambled and base-62 encoded to 7 characters. Only range allocation needs coordination; a crash just loses a few thousand codes.
Split redirect from creation. Redirects are 10x writes and need 99.99% uptime, creation 99.9%. Separate services scale and fail independently, and clicks go to Kafka asynchronously so counting never slows a redirect.
Hybrid sharding. URLs live on shards chosen by consistent hashing on short_code (fast redirects); a light user_urls index sharded by user_id serves "my URLs". Every create writes both.
| Consistency | Why | |
|---|---|---|
| Create URL | Strong (primary) | Must not lose writes |
| Redirect | Eventual (any replica) | Speed is critical |
| My URLs list | Read-your-writes | Users expect their URLs |
| Delete URL | Strong + cache invalidation | Must take effect now |
- Auto-increment codes (predictable), random-then-check (races) or UUIDs (36 characters, not 7).
- A bare 301 redirect: browsers cache it forever, hiding clicks and ignoring deletes.
- Caching URLs without their expiry, so expired links keep redirecting.