System Design Concepts

No fluff — visual, concise, interview-ready

📈 10 · SCALABILITY PATTERNS

Partitioning

Splitting data across nodes to improve scalability (horizontal growth), throughput (parallel processing), and storage capacity (distribute data), with trade-offs in complexity (routing, rebalancing) and skew (uneven distribution).

Horizontal vs Vertical Partitioning

Horizontal (Split Rows)

ID Name City 1RohitNYC 2AlexLON 3AdamBER 4JoeTKY split rows Partition 1 (rows 1-2) 1 Rohit NYC 2 Alex LON Partition 2 (rows 3-4) 3 Adam BER 4 Joe TKY Same columns, different rows per partition Most common. Each node holds subset of rows.

Vertical (Split Columns)

ID Name Bio Avatar 1Rohitlong…blob 2Alexlong…blob split columns Hot (ID + Name) 1Rohit 2Alex fast, small Cold (Bio + Avatar) 1long text…blob 2long text…blob large, rarely accessed Same rows, different columns per partition
Partitioning Strategies

Range

A — M N — Z dates Adam, Bob Nick, Zoe Jan, Feb ✓ Efficient range queries ⚠ Hot spots if skewed

Hash

hash(key) % N P0 P1 P2 ✓ Even distribution ⚠ Resharding remaps keys

List (Explicit)

region = US → P1 · EU → P2 · Asia → P3 🇺🇸 P1 🇪🇺 P2 🌏 P3 ✓ Geo-partitioning, compliance Explicit mapping per value
Guarantees: Partitioning provides linear write scaling. Partition pruning skips irrelevant partitions. Cross-partition queries are expensive (scatter-gather).
Hotspot Mitigation: Celebrity problem — fix: add random suffix → scatter across shards → aggregate on read.

Distributed Indexing

Indexing data across nodes to improve query performance (faster lookups), scalability (handle large datasets), and throughput (parallel queries), with trade-offs in consistency (index lag), write overhead (maintain indexes), and complexity (coordination).

How indexes work across partitions and shards — local vs global indexes

In a single DB, an index covers all rows. Once you partition/shard, the index must either live inside each partition (local) or span across all partitions (global). This choice fundamentally affects query routing, write latency, and consistency.
Local Index vs Global Index

Local Index (per-partition)

SELECT WHERE city='NYC' scatter to ALL Partition 1 data index city idx: NYC→row2 Partition 2 data index city idx: (none) Partition 3 data index city idx: NYC→row8 gather + merge results
Pro Fast writes — index updated locally, no cross-partition coordination
Con Non-partition-key queries → scatter-gather (query ALL partitions)
Used by: Cassandra, DynamoDB, MongoDB, Postgres partitions

Global Index (cross-partition)

SELECT WHERE city='NYC' single lookup Global Index (city) NYC → P1:row2, P3:row8 · LON → P2:row5 · BER → P1:row3 only relevant partitions Partition 1 ✓ Partition 2 (skip) Partition 3 ✓ ✓ Only 2 of 3 partitions queried
Pro Fast reads — index tells you exactly which partitions to hit
Con Slower writes — every write must update the global index (cross-partition)
Used by: DynamoDB GSI, Google Spanner, CockroachDB, Citus
AspectLocal IndexGlobal Index
Write speedFast — update local index onlySlower — must update global index (cross-partition)
Read (by partition key)Fast — single partitionFast — single partition
Read (non-partition key)Scatter-gather — query ALL partitionsTargeted — index routes to exact partitions
ConsistencyStrong (same partition)Eventual (async index update) or strong (sync, slower)
ExamplesCassandra, MongoDB, Postgres partitionsDynamoDB GSI, Spanner, CockroachDB
How Indexing Fits with Sharding & Replication
Sharding splits data across DB instances each shard has local indexes optional global index across shards Replication copies data for availability indexes replicated with data followers serve indexed reads Search (ES) inverted index across shards each shard = Lucene index scatter-gather for full-text search
Rule of thumb: Query by partition key → local index is enough (fast). Query by non-partition column → you need a global secondary index (DynamoDB GSI) or accept scatter-gather. Search workloads → use a dedicated search engine (Elasticsearch) with its own distributed inverted index.

Replication

Copying data across nodes to improve availability (failover), fault tolerance (survive failures), durability (prevent data loss), disaster recovery (region backup), and read scalability (higher read throughput), with trade-offs in consistency (stale reads), write latency (slower writes), and operational complexity (manage replicas).

Sync vs Async vs Semi-Sync Replication

Synchronous

Client Master Replica 1 Replica 2 Write write R1 write R2 wait ok ok ✓ Complete ✓ Zero data loss ✓ Strong consistency ✗ Slowest (waits for ALL)
Ex: Spanner (TrueTime), Postgres synchronous_commit=on. Banking, payments — can't lose a single write.

Asynchronous

Client Master Replica 1 Replica 2 Write ✓ Complete write R1 write R2 ok ok ✓ Fastest writes ✗ Replication lag (stale reads) ✗ Data loss if master dies
Ex: MySQL default, Postgres async replicas, MongoDB secondaries. Social media feeds, analytics — speed over durability.

Semi-Synchronous

Client Master Replica 1 Replica 2 Write write R1 wait ok (1 ACK enough) ✓ Complete write R2 (async) ok ✓ Wait for 1 replica only ✓ No data loss (1 copy safe) ⚡ Best balance of speed + safety
Ex: MySQL semi-sync, Postgres synchronous_standby_names (1 of N). E-commerce orders — need durability but not full sync overhead.
ModeClient Waits ForData Loss RiskWrite LatencyUse Case
SyncALL replicas ACKZeroHighestBanking, payments (Spanner)
Semi-Sync1 replica ACKNear-zeroMediumE-commerce, orders (MySQL semi-sync)
AsyncMaster onlyPossible (lag window)LowestSocial feeds, analytics (Postgres async)
Single-Leader Replication — Deep Dive
Write Path — All Writes Go Through Single Leader Client INSERT INTO orders ... write Leader ★ 1. Write to WAL/binlog 2. Apply to storage engine 3. Stream to followers WAL: LSN 0→1→2→3→4 Write-Ahead Log (append-only) stream WAL Follower 1 LSN: 4 (caught up) Follower 2 LSN: 3 (1 behind) Follower 3 LSN: 1 (lagging!) 👤 👤 👤 fresh read slightly stale stale read! Replication Lag Problems Read-after-write: User writes, reads from follower → sees old data Fix: Read own writes from leader, or wait for follower to catch up Monotonic reads: User reads F1 (fresh), then F2 (stale) → time goes backward Fix: Sticky sessions — always read from same follower Consistent prefix: Causal order violated across partitions Fix: Causal ordering / version vectors Failover — Leader Dies, Promote Follower ① Leader crashes heartbeat timeout (30s default) ② Detect failure Sentinel / Patroni / Orchestrator detects ③ Elect new leader Pick follower with highest LSN (most data) ④ Reconfigure Redirect writes to new leader, update DNS ⑤ Old leader returns Demoted to follower Discard unreplicated writes ⚠ Split-brain risk Both think they're leader → fencing (STONITH) Failover risks: data loss (unreplicated writes), split-brain, stale reads during election, client reconnection delay Single-Leader — When to Use Read-heavy workloads Scale reads with followers Writes bottleneck at leader Strong consistency needed Read from leader = latest data Banking, inventory, orders Simple operations No conflict resolution needed Mature tooling (Patroni, etc.) Single-region deployments Low-latency replication within DC Cross-region = high write latency Postgres: streaming replication + Patroni (auto-failover) MySQL: binlog replication + Orchestrator MongoDB: replica set (Raft-based election) Redis: master-replica + Sentinel Kafka: partition leader + ISR followers etcd/ZooKeeper: Raft/ZAB consensus leader Most common replication model — simple, well-understood, but single point of write bottleneck
Multi-Leader Replication — Deep Dive
Multi-Leader: Users Write to Nearest Leader → Low Latency 🇺🇸 US-East Region Leader A (r/w) Follower Follower 👤👤👤 US users → 5ms Writes: order_created, payment_processed Local write latency: ~5ms (same region) 🇪🇺 EU-West Region Leader B (r/w) Follower Follower 👤👤👤 EU users → 8ms Writes: user_updated, GDPR_delete Data sovereignty: EU data stays in EU 🌏 Asia-Pacific Region Leader C (r/w) Follower Follower 👤👤👤 Asia users → 6ms Writes: ride_events, IoT_telemetry Edge/IoT: write to nearest node async repl ~80ms async repl ~120ms High Availability: If US region fails → EU and Asia continue serving writes (no failover delay, active-active DR) Each region is independent — no single point of failure. Offline-first apps sync when reconnected. Write-heavy global apps: distribute write load across regions, avoid single-leader bottleneck Handling Write Conflicts — The Core Challenge Conflict Example: Same Row Updated in 2 Regions US Leader A: UPDATE users SET name='Bob' WHERE id=42 (t=100) EU Leader B: UPDATE users SET name='Robert' WHERE id=42 (t=101) Both succeed locally → conflict detected on replication! Conflict Resolution Strategies LWW Last Write Wins highest timestamp ⚠ data loss CRDTs Auto-merge types G-Counter, OR-Set ✓ no data loss Custom Logic App-level handler merge/prompt user flexible but complex Version Vectors {A:3, B:2, C:1} detect concurrent causal ordering Version Vectors: Each leader maintains a vector clock {LeaderA: 3, LeaderB: 2, LeaderC: 1}. If vector A dominates B → A is newer. If neither dominates → concurrent conflict → resolve with strategy above. Google Docs: OT (Operational Transform) — transform concurrent edits to converge. Figma: CRDTs for real-time collab. Conflict Avoidance Strategies Sticky routing: user_id=42 always writes to US leader → no cross-region conflict Partition by region: EU data → EU leader only. US data → US leader only. Schema design: append-only events (no updates) → conflicts impossible Multi-Leader Replication Topologies Circular Topology A B C D ⚠ Single node failure breaks ring Star Topology (Hub-Spoke) Hub A B C D ⚠ Hub is SPOF, but simpler routing All-to-All (Mesh) ✓ Best A B C D ✓ Most fault-tolerant, no SPOF Circular: MySQL circular replication — simple but fragile. Star: easier conflict detection at hub. All-to-All: CouchDB, Postgres BDR — most resilient. Real-world: Google Docs (OT), Figma (CRDTs), CouchDB (multi-master), Postgres BDR, MySQL Group Replication, Cassandra (peer-to-peer is leaderless but similar topology).
Leaderless Replication — Deep Dive
Leaderless: Write to ALL Replicas, Quorum Decides Success Write: W=2 of N=3 (quorum) Client PUT user:42 write Replica 1 (v2) ✓ ACK Replica 2 (v2) ✓ ACK Replica 3 (v1) ✗ offline W=2 ≥ quorum → SUCCESS Client gets OK even with 1 down Quorum Formula: W + R > N N=3, W=2, R=2 → 2+2=4 > 3 → at least 1 replica has latest version in every read Tunable: W=1,R=3 (fast writes) | W=3,R=1 (fast reads) | W=1,R=1 (eventual) Read: R=2 of N=3 (quorum) Client GET user:42 R1: v2 ✓ (latest) R2: v2 ✓ (latest) R3: v1 (stale) was offline Client picks highest version: v2 ✓ + triggers read repair on R3 Read Repair: Fix Stale Replicas on Read Client reads R3=v1 (stale) → sends v2 to R3 → R3 now has v2. Lazy repair, no background process. Anti-entropy: background Merkle tree sync catches anything read repair misses Repair Mechanisms — Keeping Replicas in Sync Read Repair On read: detect stale replica → push latest version to it ✓ Lazy, no extra traffic ✗ Only fixes data that's read Used by: Cassandra, DynamoDB, Riak Anti-Entropy (Merkle Trees) Background process compares Merkle tree hashes between replicas ✓ Catches ALL inconsistencies ✗ Background CPU/network cost Used by: Cassandra, DynamoDB, Riak Hinted Handoff Node X is down → neighbor Y stores writes as "hints" for X ✓ X comes back → Y replays hints ✗ Y runs out of space if X is down long Used by: Cassandra, Riak, Voldemort Sloppy Quorum, Version Vectors & Use Cases Sloppy Quorum If not enough "home" replicas are reachable, accept writes on ANY available node → higher availability (writes always succeed) but weaker consistency guarantee Cassandra: consistency=ANY uses sloppy quorum. DynamoDB: similar approach. Version Vectors (Crux of Leaderless) Each replica tracks: {R1: 3, R2: 2, R3: 4} — vector clock per key {R1:3, R2:2} vs {R1:2, R2:3} → concurrent! Neither dominates → conflict → resolve {R1:3, R2:2} vs {R1:2, R2:1} → first dominates → no conflict, first is newer High Availability No single point of failure Any node can serve r/w Shopping carts, sessions Tunable Consistency W=1,R=1 → eventual (fast) W=N,R=1 → strong (slow writes) Per-query consistency level Write-Heavy / Global No write bottleneck (no leader) Multi-DC: write to local replicas IoT, time-series, metrics Partition Tolerant Network splits → still serve AP in CAP theorem DNS, CDN edge caches Cassandra: CL=QUORUM (W+R>N). DynamoDB: eventually consistent reads (fast) or strongly consistent (slower). Riak: CRDTs + version vectors. Voldemort: LinkedIn's key-value store.
AspectSingle-LeaderMulti-LeaderLeaderless
Write pathAll writes → 1 leaderWrites → nearest leaderWrites → ALL replicas (W of N)
Read scalingFollowers serve readsLocal leader + followersAny replica serves reads
Write scalingBottleneck at leaderDistributed across leadersNo bottleneck (no leader)
ConsistencyStrong (read from leader)Eventual (conflicts possible)Tunable (W+R>N = strong)
ConflictsNone (single writer)Yes — LWW, CRDTs, customYes — version vectors, CRDTs
FailoverElection needed (downtime)Other leaders continueNo failover needed
ComplexitySimpleHigh (conflict resolution)Medium (quorum tuning)
Best forRead-heavy, single-regionMulti-region writes, collabHigh availability, write-heavy
ExamplesPostgres, MySQL, MongoDB, RedisCouchDB, Postgres BDR, Google DocsCassandra, DynamoDB, Riak
Decision Guide: Need strong consistency + simple ops? → Single-Leader. Need multi-region low-latency writes? → Multi-Leader. Need maximum availability + no SPOF? → Leaderless. Most systems start single-leader and evolve as scale demands.

Sharding

Horizontal partitioning across independent database instances to improve write scalability (distribute writes), read scalability (parallel reads), and storage capacity (split data), with trade-offs in complexity (routing, rebalancing), cross-shard queries (expensive joins), and hotspots (uneven key distribution).

Sharding vs Replication

Sharding (Split Data)

1 Large DB Shard 1 Shard 2 Shard 3 Each shard holds different rows Write scaling ✓

Replication (Copy Data)

1 Large DB Replica 1 Replica 2 Replica 3 Each replica holds same data (copy) Read scaling ✓
Range vs Hash (Key-Based) Sharding

Range-Based Sharding

Name Age Rohit28 Alex32 Adam34 John45 Anshu50 20 < age ≤ 30 Rohit28 1 row 30 < age ≤ 40 Alex32 Adam34 2 rows 40 < age ≤ 50 John45 Anshu50 2 rows ⚠ Uneven distribution — hot spots possible

Key-Based (Hash) Sharding

ID Name Last 1RohitSehgal 2AlexXu 3FooBar 4AdamDoe 5JoeJonas ID % 3 1%3=1 · 2%3=2 · 3%3=0 · 4%3=1 · 5%3=2 hash = 0 3FooBar hash = 1 1RohitS. 4AdamD. hash = 2 2AlexXu 5JoeJ. ✓ Even distribution — almost equal partitions
Directory & Geo Sharding

Directory-Based Sharding

App 1 Lookup Table user_1 → S1 user_2 → S2 user_3 → S3 2 Shard 1 Shard 2 Shard 3 ✓ Flexible — any key→shard mapping ⚠ Lookup table = SPOF + bottleneck

Geo-Based Sharding

👤US user 👤EU user 👤Asia user Geo Router 🇺🇸 US Shard us-east-1 🇪🇺 EU Shard eu-west-1 🌏 Asia Shard ap-south-1 ✓ Low latency + GDPR compliance
StrategyHowGuaranteeRisk
Hashhash(key) % NEven distributionResharding requires data migration
RangeSplit by value range (age, date)Efficient range queries within shardHot spots for sequential keys
DirectoryLookup table maps key → shardFlexible, any mappingLookup table = SPOF
GeoRegion-based (US→shard1, EU→shard2)Data locality + complianceCross-region queries expensive
Real-world: Instagram shards Postgres by user_id (hash). Vitess (YouTube) — MySQL sharding middleware. Citus — distributed Postgres.

Consistent Hashing & Bounded Load

Map keys to nodes via a hash ring to improve scalability (easy node add/remove), load distribution (even spread with virtual nodes), and stability (minimal key movement), with trade-offs in complexity (ring management) and hotspots (skewed keys).

1 The Problem with Modulo Hashing

With 3 servers

server = hash(key) % 3
S1
S2
S3
K1→S1
K2→S2
K3→S3
K4→S1
K5→S2
K6→S3

Add 1 server (N=3 → 4)

server = hash(key) % 4
S1
S2
S3
S4
K1→S2
K2→S3
K3→S4
K4→S1
K5→S2
K6→S3
⚠ Almost all keys remapped → massive data movement, cache invalidation, latency spikes
2 Consistent Hashing — The Basic Idea
  1. Hash servers onto a ring (0 to 2³²)
  2. Hash keys onto the same ring
  3. Each key goes to the first server clockwise
0 / 2³² 75 150 225 S1 S2 S3 K1 K2 K3 K4 K5 K6

Key → Server Mapping

K1S1 K2S1 K3S2 K4S3 K5S3 K6S1
S1 owns range (225, 75]
S2 owns range (75, 150]
S3 owns range (150, 225]
Only keys in a server's range belong to that server
3 What Happens When the Cluster Changes?

Add S4 at position 200

S1 S2 S3 S4 K5 0
K5 was on S3 → now on S4
All other keys unchanged
Only keys in (150, 200] move from S3 → S4

Remove S2

S1 S2 S3 K3 0
K3 was on S2 → now on S3 (next clockwise)
All other keys unchanged
Keys in (75, 150] move to next server clockwise
4 Why Virtual Nodes?

Without vnodes (3 servers, 1 point each)

A B C
A
60%
B
30%
C
10%
Uneven — A gets 6x more than C

With vnodes (each server × N points)

A A A B B B C C C
A
~33%
B
~33%
C
~33%
Even — many small ranges balance load
Virtual nodes = better balance + smoother failover + supports weighted capacity (bigger server → more vnodes)
5 How the Ring is Built (High Level)
  1. Discover servers in cluster — get the list of active nodes
  2. Generate virtual nodeshash(serverId + i) for i = 1..N
  3. Sort points on the ring — keep a sorted structure (e.g., array / skip list)
  4. Locate keyhash(key) → find first node clockwise (binary search)
  5. Handle replicas (optional) — pick next R distinct physical nodes on the ring for replication
6 Where is it Used?
SystemHow it Uses Consistent Hashing
Caching (Redis Cluster, Memcached)Stable key-to-node mapping, minimal cache churn on scale
Databases / KV Stores (DynamoDB, Cassandra)Sharding data with minimal resharding
Rate LimitingSame user always hits the same limiter instance
CDN / Load BalancingSame content routes to the same cache node
Message Brokers (Kafka)Partition ownership with minimal movement
7 Bounded Load (Google, 2017)

Problem: Hot Keys

S1
20%
S2
15%
S3
65% 🔥
Even with vnodes, hash collisions or hot keys can overload one node

Fix: Cap at (1+ε) × avg

S1
30%
S2
32%
S3
38% ✓
If node is "full", key spills to next node on ring. No node exceeds cap.

✓ Key Takeaways

Minimizes movement when nodes change
Virtual nodes essential for even distribution
Solves placement stability, not hot keys
Topology consistency across clients is critical
Think: minimize disruption under change

⚠ Watch Out For

Hot keys can still overwhelm a node
Data size imbalance (not just key count)
Different ring views → inconsistent routing
Too few virtual nodes → uneven load
Rebalancing is not free — do it intentionally
Mental Model: Consistent hashing is not about hashing. It's about stable key placement under change.

Bloom Filters

A probabilistic data structure that tells you: "Definitely not in the set" or "Probably in the set" (false positives possible, false negatives never). Uses tiny memory for ultra-fast checks at massive scale.

Real-Time Example: "Is this username taken?" (Instagram/Twitter signup)
Scenario: User signs up → check if "@elonmusk" is already taken (500M usernames in DB) User types: "@elonmusk" 3 Hash Functions (k=3) H1: MurmurHash("@elonmusk") % 20 = 3 H2: xxHash("@elonmusk") % 20 = 11 H3: FNV1a("@elonmusk") % 20 = 17 Bloom Filter Bit Array (m=20, holds 500M usernames) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 1 0 1 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 All 3 bits (pos 3, 11, 17) = 1 → ⚠️ PROBABLY TAKEN Confirm with DB query (only for "maybe" results) Compare: "@suresh_dev_2024" → H1→pos 7=0 → ❌ DEFINITELY AVAILABLE (no DB query needed! saved ~5ms)
Why this matters at scale: Instagram has 500M+ usernames. Without Bloom filter: every signup checks DB (5ms × millions of signups/day = massive DB load). With Bloom filter: 99% of "is this taken?" checks answered in ~0.01ms from memory — only the 1% "maybe taken" results hit the DB. Filter size: 500M × 10 bits = ~600MB RAM (fits on one server).
Lookup Process

Query: "bob" → Definitely NOT in set

"bob" H1→pos 1 1 H2→pos 4 0 ❌ DEFINITELY NOT Any bit = 0 → item is 100% absent

Query: "user123" → Probably in set

"user123" H1→1 1 H2→6 1 H3→11 1 ✓✓✓ ⚠ PROBABLY YES All bits = 1 → might be present (false positive possible)
Key Insight: If ANY bit is 0Definitely NOT present (zero false negatives). If ALL bits are 1Might be present (false positive possible). Memory: ~100KB for 1M items · O(K) insert + lookup.
Why It Matters — Real-World

Cassandra / Bigtable

"Is key in this SSTable?" Check filter first → skip disk read if NO. Saves millions of I/O ops.

Chrome Safe Browsing

"Is this URL malware?" Check local bloom filter → only fetch from server if MAYBE. No network call for 99% of URLs.

Medium / Recommendations

"Has user read this article?" Filter per user → never recommend already-read content. Tiny memory per user.

CDN / Cache Check

"Is content cached at this edge?" Check filter → skip origin fetch if definitely not cached. Reduces origin load.
Real-Time Example: Cassandra Read Path with Bloom Filter
Client GET user:789 Memtable (in-memory) miss ✗ Bloom Filter Check — Which SSTable has this key? SSTable-1 Bloom Filter H1(user:789)→pos 3 = 0 ❌ SKIP (no disk read) Saved ~10ms I/O! SSTable-2 Bloom Filter H2(user:789)→pos 9 = 0 ❌ SKIP (no disk read) Saved ~10ms I/O! SSTable-3 Bloom Filter All bits = 1,1,1 ✅ MAYBE → read disk Only 1 disk read needed ✓ Found on disk! Return to client Result: 1 disk read instead of 3 Bloom filter saved 67% of I/O
Multiple Hash Functions — Why K=3 or K=7?
ParameterSymbolMeaningTypical ValueImpact
Bit array sizemTotal bits in the filter~10 bits per itemMore bits → fewer false positives, more memory
Number of itemsnExpected items to insertApplication-specificMore items → higher false positive rate
Hash functionskNumber of independent hashesk = (m/n) × ln(2) ≈ 7Optimal k minimizes false positive rate
False positive ratepP(says "yes" when item absent)~1% at m/n=10Lower p → need more bits per item

Common Hash Functions Used

MurmurHash3: Fast, good distribution. Used by Cassandra, Guava.
xxHash: Extremely fast (>10 GB/s). Used by Redis, LZ4.
FNV-1a: Simple, decent distribution. Good for small filters.
Double hashing trick: Use only 2 hash functions → derive K hashes as:
h_i(x) = h1(x) + i × h2(x) (for i = 0..k-1)
This gives K independent hashes from just 2 computations!

False Positive Rate by Configuration

m/n = 5, k=3: FP rate ≈ 10% (too high for most uses)
m/n = 10, k=7: FP rate ≈ 0.82% (good default)
m/n = 15, k=10: FP rate ≈ 0.03% (very accurate)
m/n = 20, k=14: FP rate ≈ 0.001% (near-perfect)

Formula: p ≈ (1 - e^(-kn/m))^k
Memory: 1M items × 10 bits = ~1.2 MB. Tiny!
Scaling Problem: What happens when data grows beyond initial m?
The problem: A standard Bloom filter has fixed size m (set at creation). You can't resize it — adding more bits would invalidate all existing hash positions. As n (items) grows beyond what m was designed for, the false positive rate degrades rapidly. Designed for 1M items but now have 10M? FP rate jumps from 1% to ~40%.

Solution 1: Scalable Bloom Filter

How: Chain multiple filters. When filter #1 fills up (FP rate exceeds threshold), create filter #2 with tighter FP rate (e.g., p/2). New inserts go to latest filter. Lookup checks ALL filters.
Tradeoff: Lookup is O(K × number_of_filters) — slightly slower as it grows.
Used by: Redis BF.RESERVE with expansion option.

Solution 2: Rebuild with larger m

How: When FP rate exceeds threshold, create a new filter with 2× the bits. Re-insert all items from the source of truth (DB). Swap atomically.
Tradeoff: Requires access to original data. Downtime or dual-read during rebuild.
Used by: Cassandra (rebuilds on compaction), periodic maintenance jobs.

Solution 3: Partitioned / Sharded Filters

How: Shard data by key range or hash. Each shard has its own Bloom filter sized for its subset. Add shards as data grows — each filter stays within its designed capacity.
Tradeoff: Lookup must route to correct shard first.
Used by: Cassandra (per-SSTable filter), HBase (per-region filter).
Practical rule: Size your initial filter for 2-3× expected items (headroom for growth). Monitor FP rate. When it crosses your threshold (e.g., >5%), trigger rebuild or chain a new filter. In Cassandra, this happens automatically during compaction — old SSTables merge → new Bloom filter built for the merged data.
Limitations: Cannot delete items (setting bit to 0 might affect other items). Cannot count (only membership). Cannot resize (must rebuild with larger m). No enumeration (can't list what's in the filter).
Variants: Counting Bloom Filter — uses counters instead of bits → supports delete (at 4× memory). Cuckoo Filter — supports delete + slightly better space efficiency. Scalable Bloom Filter — grows dynamically by chaining multiple filters. Quotient Filter — cache-friendly, supports merge + resize.
One-liner: Bloom filter = K hash functions mapping items to M bits. Insert: set K bits. Lookup: check K bits. Any 0 → definitely absent. All 1 → probably present. Use m/n=10, k=7 for ~1% FP rate. Real-world: Cassandra skips disk reads, Chrome blocks malware URLs, CDNs avoid origin fetches.

Rate Limiting

Throttle requests to improve availability (prevent overload), fairness (per-user limits), and cost control (limit usage), with trade-offs in user experience (rejected requests) — return 429 Too Many Requests.

🌐Client Rate Limiter check rules Redis counter ✓ allow API Server 429 ✗ Rules Config

Fixed Window

Capacity: 3 per window 02:00 02:10 02:20 OK Dropped
Simple counter per window. Edge burst: 2× at window boundary.

Sliding Window

Previous min Current min Sliding Window 2/3rd 1/3rd Limit: 10/min
Weighted blend of prev + current window. No edge burst, memory efficient.

Token Bucket

new tokens add at fixed rate Bucket Request token? ✗ drop (no tokens left)
Allows bursts up to bucket size. Most common (AWS, Stripe).

Leaky Bucket

incoming FIFO Queue fixed rate Srv ✗ overflow (queue full → drop)
Smooth output, no bursts. Queue absorbs spikes, fixed drain rate.
AlgorithmHow It WorksBurst?MemoryAccuracyBest ForReal-World
Fixed Window
→ Simple but unfair
Counter resets every N seconds (e.g., every minute). Simple INCR + EXPIRE.2× at edge — user sends max at end of window + start of nextLow — 1 counter per keyApproximateQuick to implement, acceptable when occasional unfairness at window edges is fineTwitter/X → 300 tweets/3hr
Basic NGINX limit_req zone
Sliding Window Log
→ Fair and exact
Store timestamp of each request in sorted set. Count entries within last N seconds.NoHigh — stores every request timestampExactMust guarantee exact count per user — no one gets even 1 extra request throughFinancial APIs → strict per-second limits
Auth endpoints → brute-force prevention
Sliding Window Counter
→ Fair and scalable
Weighted blend: prev_window × overlap% + current_window countNoLow — 2 counters per keyAccurate (approx)Need fair enforcement without storing every timestamp — best tradeoff for high-volume APIsGitHub API → 5000 req/hr
Shopify → 40 req/sec per app
Token Bucket
→ Controlled bursts
Bucket fills at fixed rate. Each request consumes 1 token. Empty = reject.Yes (controlled) — burst up to bucket capacityLow — 2 values (tokens + last_refill)GoodAllow short spikes while enforcing average rate — users can burst then slow down naturallyAWS API Gateway → token bucket per stage
Stripe → 100/sec burst allowed
NGINX limit_req with burst param
Leaky Bucket
→ Smooth output
FIFO queue drains at fixed rate. Overflow = reject. Smooths output.No — perfectly smooth outputQueue sizeExactDownstream service needs steady traffic, not spikes — protect fragile backends from burstsUber → dispatch queue (smooth ride assignment)
Twilio → SMS sending at fixed rate
Network routers → QoS shaping
1. What Are We Limiting?
GranularityKey PatternWhere AppliedExampleWhy This Level
Per Userrate_limit:user:123Application / GatewayInstagram → 100 posts/day per userOne user shouldn't spam. Others unaffected.
Per IPrate_limit:ip:1.2.3.4Edge / CDN / GatewayLogin page → 5 attempts/min per IPUser identity unknown (pre-auth). Used for login, OTP, signup.
Per API Keyrate_limit:apikey:abc123API GatewayStripe → Partner A: 1000/sec, Partner B: 100/secEach customer pays differently. Tied to billing tier.
Per Endpointrate_limit:endpoint:/generateApplication / GatewayAI image gen → /profile: 1000/min, /generate: 10/minNot all APIs cost the same. Expensive endpoints need stricter limits.
Globalrate_limit:service:totalLoad Balancer / GatewayDB protection → entire service max 50K req/secProtect backend from total overload regardless of who sends.
2. What's the Rule?

Single Rule

100 requests/minute
Example: Weather API, public read endpoints
Simple. One counter, one threshold.

Stacked Rules (Multi-Window)

10/sec + 100/min + 1000/hour
Example: GitHub API
Prevents both: short spikes (10/sec) AND long-term abuse (1000/hr).
User can't send 100 instantly OR run forever at high rate.
3. What Happens When Limit Hits?
ResponseBehaviorExampleWhen to Use
Reject (429)Immediately return 429 Too Many RequestsGitHub, Stripe, public APIsSimple, cheap, predictable. Client retries with backoff.
QueueAccept request, process later from queueTwilio SMS, email sending (100K emails queued)User wants eventual delivery. Not time-critical.
ThrottleDegrade quality instead of failingNetflix → reduce video bitrate instead of stoppingBetter UX than hard rejection. Graceful degradation.
4. Tier-Based Limits
Limits tied to revenue: Free → 100 req/hr • Premium → 10,000 req/hr • Enterprise → 100,000 req/hr. Common in: SaaS APIs, AI platforms (OpenAI, Anthropic), cloud services (AWS). Store tier in user profile, look up at gateway.
5. How to Pick the Right Algorithm
NFR QuestionIf This...Then Choose...Why
Scale?Small (100 users, internal)Sliding Window LogExact, simple, memory is fine
Huge (10M+ users)Sliding Counter or Token BucketLog stores every timestamp → memory explosion at scale
Single or Distributed?One serverIn-memory counterNo network hop needed
Multiple servers (Uber, Netflix)Redis (shared state)All nodes must see same count
Accuracy?Security-critical (login, OTP)Sliding Window LogEven 2 extra attempts matter
Approximate fine (social posting)Sliding Counter / Token Bucket101 instead of 100 → not a disaster
Latency budget?Ultra-sensitive (HFT, gaming)Local in-memory limiterRedis lookup adds 2-5ms
Normal APIs (e-commerce, SaaS)Redis2-5ms acceptable
Limiter store fails?Fail-Open (allow traffic)Public APIs, product catalog, newsAvailability > protection. Risk abuse over downtime.
Fail-Closed (reject traffic)Bank login, OTP, admin portalSecurity > availability. Block all over unlimited attacks.
6. Implementation (Distributed — Redis)
Client API key: sk_live_... API Gateway NGINX / Kong / Envoy rate limit middleware Redis INCR + EXPIRE (atomic) ✔ allow Backend process request ✗ 429 429 Too Many Requests Retry-After: 30 Redis Sliding Window (Lua atomic): MULTI ZREMRANGEBYSCORE key 0 (now - window) ZADD key now now ZCARD key → count > limit? reject Response Headers (RFC 6585): X-RateLimit-Limit: 100 X-RateLimit-Remaining: 23 X-RateLimit-Reset: 1704067260 Retry-After: 30
Distributed challenges: Race conditions — two nodes check simultaneously. Fix: Redis Lua script (atomic). Clock skew — use Redis server time. Redis failure — fail-open (allow) for public APIs, fail-closed (block) for security-critical.
Anti-patterns: Rate limit only at app layer — DDoS bypasses. No Retry-After header — clients hammer blindly. Same limit for all endpoints — /login needs stricter limits than /search. No differentiation — paid users get same limits as free.
Interview tip: "I'd put rate limiting at the API Gateway layer using Redis sorted sets for a sliding window. Per-user limits with API key, per-IP limits for unauthenticated. Return 429 with Retry-After header. Fail-open on Redis failure to avoid blocking all traffic."

Backpressure

When the consumer can't keep up with the producer — the system must decide: slow down, buffer, or shed load

Backpressure — Detection & Response Strategies
Producer 10,000 msg/sec ⚡ TOO FAST flood Buffer / Queue 83% FULL ⚠ bounded: 10K messages alarm: depth > 80% Consumer 2,000 msg/sec 🐢 TOO SLOW Response Strategies ① Drop / Load Shed (503) discard oldest or lowest-priority Use: real-time metrics, ad bids, live video ② Slow Producer (flow control) TCP window, HTTP 429, Kafka quota Use: lossless pipelines, API rate limiting ③ Bounded Buffer + Block producer blocks when buffer full Use: Reactive Streams, Go channels, Akka ④ Scale Consumers (auto-scale) add more consumers / partitions Use: Kafka consumer groups, KEDA, SQS+Lambda ⑤ Sample / Aggregate process 1-in-N, batch + flush Use: telemetry, logging, analytics backpressure signal propagates upstream
StrategyData Loss?Latency ImpactWhen to UseExample
Drop / Load ShedYesNone (fast)Real-time, freshness > completenessLive metrics, ad bids, video frames
Bounded Buffer + BlockNoIncreases (producer waits)Lossless, can tolerate delayReactive Streams, Go channels, Akka
Slow Producer (flow control)NoPropagates upstreamEnd-to-end flow controlTCP window, Kafka quotas, HTTP/2
Scale ConsumersNoTemporary lag during scale-upElastic workloadsKafka consumer groups, KEDA, Lambda
Sample / AggregatePartialNoneHigh-volume telemetryStatsD (1-in-10), log sampling
Priority QueueLow-priority droppedNone for high-priorityMixed criticality trafficPayment > analytics, VIP > free tier
Backpressure Implementation by System

Network / Transport

  • TCP: sliding window shrinks when receiver is slow
  • HTTP/2: WINDOW_UPDATE frames control flow
  • gRPC: flow control per stream
  • WebSocket: application-level ACKs

Message Brokers

  • Kafka: consumer lag metric, fetch.max.bytes
  • RabbitMQ: prefetch count, credit-based flow
  • SQS: visibility timeout + DLQ
  • NATS: slow consumer detection, disconnect

Application Frameworks

  • Reactive Streams: request(N) protocol
  • Go channels: bounded channel blocks sender
  • Node.js streams: highWaterMark + drain event
  • Akka Streams: async boundaries with buffers
Key Insight: Always use bounded buffers. Unbounded queues are how outages turn into OOMs. Set buffer size based on: acceptable latency × throughput rate. Alarm on queue depth — growing depth means consumer is falling behind.
Monitoring: Track consumer lag (Kafka), queue depth (SQS/RabbitMQ), buffer utilization %, and drop rate. Set alerts at 60% capacity (warning) and 80% (critical). Use these signals to trigger auto-scaling.
Anti-patterns: Unbounded buffers — OOM guaranteed under sustained load. No backpressure signal — producer keeps flooding. Dropping without metrics — silent data loss. Blocking in async code — deadlocks the event loop.
Real-world: Netflix — RxJava backpressure for streaming pipeline. Uber — QALM (queue-based adaptive load management). Twitter — Finagle admission control rejects when overloaded. Kafka — consumer lag triggers auto-scaling via KEDA.

Auto-Scaling

Match capacity to load — reactively (metrics-driven) or predictively (ML/schedule-based). Scale up fast, scale down slow.

Auto-Scaling — Signals, Decisions & Actions
Scaling Signals CPU Utilization > 70% simple, universal, laggy Queue Depth / Consumer Lag SQS, Kafka lag → KEDA Request Rate / Latency p99 closest to user experience Memory / Disk / Network I/O resource-specific bottlenecks Custom Metrics (business) active users, orders/min Schedule / Predictive (ML) Scaling Policy Target Tracking (maintain 70% CPU) Step Scaling (thresholds) Predictive (ML forecast) Scheduled (cron-based) Scaling Actions ⬆ Scale Up add instances fast (30-60s) aggressive threshold cooldown: 60s ⬇ Scale Down remove instances slow (drain first) conservative threshold cooldown: 300s Cluster Autoscaler (nodes) add/remove nodes when pods can't be scheduled Pre-warming (predictive) scale before traffic arrives (ML / schedule) Scaling Best Practices ✓ Scale up fast (1 min) ✓ Scale down slow (5 min) ✓ Combine multiple signals ✓ Set min/max instance bounds ✓ Use cooldown periods ✓ Test with load before prod Golden rule: scale up on leading indicators (queue depth), scale down on trailing (CPU)
PlatformToolSignalsKey Feature
KubernetesHPA (Horizontal Pod Autoscaler)CPU, memory, custom metricsNative, works with any metric via Prometheus adapter
KubernetesKEDAQueue depth, Kafka lag, cron, HTTPScale to zero, 50+ scalers, event-driven
AWSASG + Target TrackingCPU, ALB request count, custom CWPredictive scaling, warm pools, mixed instances
AWSLambda (implicit)Concurrent invocationsInstant scale, per-request billing, 1000 concurrent default
GCPManaged Instance GroupsCPU, HTTP LB, Pub/Sub backlogPredictive autoscaling, per-instance metrics
AzureVMSS AutoscaleCPU, queue, custom metricsScale-in policy (newest/oldest first), overprovisioning
Scaling Patterns & Considerations

Horizontal vs Vertical

  • Horizontal: add more instances (preferred, no ceiling)
  • Vertical: bigger instance (simpler, has ceiling)
  • Stateless services → horizontal easy
  • Stateful (DB) → vertical first, then shard
  • Combine: vertical for DB, horizontal for app tier

Cold Start & Warm Pools

  • Cold start: new instance needs boot + warm cache
  • Warm pools: pre-initialized instances ready to serve
  • Predictive: ML forecasts traffic, pre-scales
  • Scheduled: scale before known peaks (9am, sales)
  • Lambda cold start: 100ms-10s depending on runtime
Combine signals: Base on CPU for steady state, but use queue depth for burst detection (leading indicator). Add predictive scaling to pre-warm before known peaks. Set min instances to handle baseline + sudden spikes during scale-up lag.
Scale-down safety: Use connection draining (deregister from LB, finish in-flight requests). Set termination grace period (K8s: preStop hook). Scale down 1 at a time to avoid cascading failures. Never scale below N+1 capacity (survive 1 instance failure).
Anti-patterns: Scaling on a single metric — CPU high but memory is the bottleneck. No cooldown — thrashing (scale up/down/up/down). No max limit — runaway scaling = runaway cost. Scaling stateful services without draining — data loss.
Real-world: Netflix — Titus auto-scaling with predictive models (pre-warm for evening peak). Uber — Peloton scheduler scales 4M+ containers. Shopify — scales to 80K+ RPS during flash sales using predictive + reactive. Slack — KEDA scales workers based on job queue depth.

Graceful Degradation

Stay partially up instead of fully down — shed non-critical features to protect core user journeys

Degradation Tiers — Progressive Feature Shedding
LOAD 100% 0% HEALTHY All features active ✓ Recommendations ✓ Analytics ✓ Search suggestions ✓ Real-time updates ✓ Personalization ✓ A/B tests ✓ Chat support ✓ Full API STRESSED Disable non-essential ✓ Core features ✓ Serve stale cache ✗ Recommendations OFF ✗ Analytics delayed ✗ A/B tests paused ✗ Non-critical APIs throttled OVERLOADED Read-only mode ✓ Browse catalog ✓ View orders ✗ New orders queued ✗ Search simplified ✗ Images low-res ✗ Free tier blocked CRITICAL Core only ✓ Login ✓ Checkout ✓ Payment processing ✗ Everything else OFF Static error page for non-critical paths
Degradation Tactics & Implementation
TacticHow It WorksImplementationExample
Feature FlagsToggle features on/off without deployLaunchDarkly, Unleash, FlagsmithDisable recommendations when DB is slow
Circuit BreakerStop calling failing dependencyHystrix, Resilience4j, PollyPayment service down → queue orders
Cached FallbackServe stale data when source unavailableRedis/CDN with TTL overrideProduct catalog from cache when DB is down
Request HedgingSend to multiple backends, use first responsegRPC hedging policyTail latency reduction (p99 → p50)
Traffic TieringPrioritize paid/VIP over free tierPriority queues, weighted routingPaid users get full features, free get basic
Static FallbackServe pre-rendered static pageCDN origin failover, S3 static siteHomepage from CDN when app servers are down
Bulkhead IsolationIsolate failures to one componentSeparate thread pools, pods, clustersSearch failure doesn't affect checkout
Implementation Patterns

Circuit Breaker States

  • Closed: normal operation, counting failures
  • Open: fast-fail all requests (serve fallback)
  • Half-Open: allow 1 request to test recovery
  • Thresholds: 5 failures in 10s → open for 30s
  • Per-dependency circuit breakers (not global)

Degradation Decision Matrix

  • CPU > 80%: disable recommendations, analytics
  • DB latency > 500ms: serve from cache only
  • Error rate > 5%: circuit break failing service
  • Queue depth > 10K: reject new non-critical work
  • Memory > 90%: drop sessions, reduce cache
Tactics: Feature flags (LaunchDarkly) for instant toggle. Traffic tiering by importance (paid > free). Cached fallbacks with extended TTL. Circuit breakers per dependency. Request hedging for tail latency. Bulkhead isolation — failure in one component doesn't cascade.
Testing degradation: Chaos engineering — inject failures in prod (Netflix Chaos Monkey). Game days — simulate outages with the team. Load testing — push past capacity to verify degradation tiers activate correctly. Dependency kill switch — test each circuit breaker independently.
Anti-patterns: All-or-nothing — either fully up or fully down (no middle ground). No fallback tested — fallback path has its own bugs. Cascading failures — one service down takes everything down. Manual intervention required — degradation should be automatic.
Real-world: Netflix — Hystrix circuit breakers + fallback to cached recommendations. Amazon — static product pages from S3 when app tier is overloaded. Twitter — fail whale → degraded timeline (no trends, no who-to-follow). Shopify — checkout always works, even if storefront is degraded during flash sales.