System Design (LLD)
Core concepts, architecture patterns, resilience, caching, databases, and component-level interview questions. For big-infra HLD questions, see High-Level Design β
Typical Scalable Web Architecture
Mobile App
Web Browser
3rd Party API
β
CDNStatic Assets
DNS / AnycastGlobal Routing
β
Load BalancerL7 β NGINX / ALB
β
API GatewayAuth Β· Rate Limit Β· Route Β· Log
β
User Service
Order Service
Product Service
Notif Service
β
KafkaAsync Messaging
Redis ClusterCache Β· Sessions
β
MySQL / PostgresACID Transactions
CassandraWrite-Heavy
ElasticsearchFull-Text Search
S3Blob Storage
β async
Background WorkersJobs Β· ETL Β· Emails
AnalyticsSnowflake Β· BigQuery
Fundamentals
Databases
Caching
Microservices
Resilience
Distributed
Networking
Auth & Security
Interview Questions
HLD Questions β
Core Fundamentals
CAP Theorem
Pick any 2: Consistency Availability Partition Tolerance
- Consistency Every read returns the most recent write
- Availability Every request gets a non-error response
- Partition Tolerance System works despite network partitions
| System | Type |
|---|---|
| PostgreSQL, MySQL | CA (single node) |
| Cassandra, DynamoDB | AP (eventual) |
| HBase, Zookeeper, etcd | CP (may reject) |
| MongoDB (default) | CP (tunable) |
| Couchbase | AP or CP (configurable) |
Reality
P is always required in distributed systems. The real choice is C vs A during a partition.ACID vs BASE
| ACID (SQL) | BASE (NoSQL) |
|---|---|
| Atomicity all or nothing | Basically Available |
| Consistency valid state always | Soft State |
| Isolation no interference | Eventually Consistent |
| Durability committed = safe | Prioritizes availability |
Use ACID: banking, bookings, inventory. Use BASE: social feeds, analytics, caching, data catalog metadata.
Scalability Vertical vs Horizontal + Strategies
Vertical Scaling (Scale Up)
- Add CPU/RAM to existing server
- Simple no application changes
- Single point of failure
- Hard hardware limit, expensive
Horizontal Scaling (Scale Out)
- Add more commodity machines
- Theoretically unlimited
- Requires stateless services + LB
- Complex: distributed state, consistency
Key Scaling Strategies
- Stateless Services: Store state in Redis/DB. Any instance handles any request.
- Read Replicas: Primary for writes, replicas for reads (80% of traffic is reads)
- Sharding: Partition data across nodes by shard key
- CDN: Cache static assets globally, reduce origin server load drastically
- Async Processing: Queue slow work (emails, reports) instant API response
- Denormalization: Pre-compute joins faster reads at cost of storage + write complexity
PACELC Theorem (CAP Extension)
If (P)artition: choose (A) or (C). Else (E): choose (L)atency or (C)onsistency
- CAP only models partition scenarios. PACELC says: even normally, there is a latency vs consistency tradeoff.
- Replication = lower latency OR stronger consistency never both.
| System | Partition choice | Else choice |
|---|---|---|
| DynamoDB | Availability | Latency (low) |
| Cassandra | Availability | Latency (low) |
| Google Spanner | Consistency | Consistency (strong) |
| MySQL | Consistency | Consistency |
Databases
Database Indexing B-Tree, Hash, Composite
Must Know
| Type | Structure | Best For | Not For |
|---|---|---|---|
| B-Tree | Balanced tree | Range queries, ORDER BY, <, >, BETWEEN | High-cardinality exact lookups (hash faster) |
| Hash Index | Hash table | Exact equality (=), O(1) | Range queries, sorting |
| Composite | B-Tree on N cols | Multi-column queries (leftmost prefix rule) | Queries not starting with leftmost column |
| Covering Index | Index has all queried cols | Satisfy query from index alone (no heap fetch) | Wide tables with many columns |
| Full-Text | Inverted index | LIKE '%word%', text search | Exact match (regular index faster) |
| Partial Index | Index with WHERE clause | WHERE status='active' index only active rows | When all rows need indexing |
Index Trade-offs
- Reads faster: SELECT queries benefit dramatically (O(log n) vs O(n))
- Writes slower: every INSERT/UPDATE/DELETE updates all indexes
- Storage cost: indexes use ~20-30% extra disk space each
- Cardinality: index on gender (2 values) = nearly useless; index on user_id = excellent
Leftmost Prefix Rule
Composite index (a, b, c) helps queries on: a (a,b) (a,b,c). Does NOT help: (b) (c) (b,c) alone.SQL vs NoSQL When to Use What
| Factor | SQL (Relational) | NoSQL |
|---|---|---|
| Schema | Fixed, enforced (DDL) | Flexible, schema-on-read |
| Relationships | Foreign keys, JOINs | Denormalized, embedded |
| Horizontal Scaling | Difficult (sharding complex) | Native (Cassandra, Mongo) |
| Transactions | Full ACID | Limited / eventual |
| Query Language | SQL (very expressive) | API-specific, limited joins |
NoSQL Types & Use Cases
- Key-Value (Redis, DynamoDB): sessions, cache, feature flags
- Document (MongoDB): product catalogs, CMS, user profiles
- Wide-Column (Cassandra, HBase): IoT, time-series, write-heavy analytics
- Graph (Neo4j): social network, knowledge graph, data lineage
- Search (Elasticsearch): full-text search, log analytics
- Time-Series (InfluxDB, TimescaleDB): metrics, monitoring
Choose SQL When
- Complex queries with multiple JOINs
- ACID transactions are critical (money, inventory)
- Schema is stable and well-defined
- Rich aggregation / reporting needs
- E.g.: banking, ERP, booking, HR systems
- Choose NoSQL when schema evolves rapidly or write throughput is extreme
Database Replication Strategies
Database Replication β Single Leader (Primary-Replica)
App Server 1
App Server 2
App Server 3
β WRITES only
β READS (scale-out)
PrimaryAll writes go here
β sync / async β
Replica 1reads
Replica 2reads
Replica 3reads
Single Leader (Primary-Replica):
Leader (writes) async/sync replication Replica 1, 2, 3
Reads can go to any replica (scale reads 3x)
Multi-Leader:
Leader A (DC West) replication Leader B (DC East)
Both accept writes; conflict resolution required
Leaderless (Dynamo-style):
Client writes to N nodes; quorum W > N/2
Client reads from R nodes; R + W > N = consistency
| Strategy | Pros | Cons |
|---|---|---|
| Single Leader | Simple, no conflicts, ordered writes | Leader bottleneck, failover needed |
| Multi-Leader | Write availability in multiple DCs | Conflict resolution required |
| Leaderless | High availability, no SPOF | Complex quorums, eventual consistency |
Sync vs Async Replication
- Synchronous: write confirmed only when all replicas ack safe but slow
- Asynchronous: write confirmed on leader write fast but potential data loss on leader crash
- Semi-sync: at least 1 replica acks balance of safety and speed (MySQL default)
Database Sharding Strategies & Pitfalls
| Strategy | How | Pros | Cons |
|---|---|---|---|
| Range Sharding | A-M shard1, N-Z shard2 | Simple range queries | Hotspots (all celebrities on one shard) |
| Hash Sharding | hash(key) % N | Even distribution | Rebalancing when N changes remaps many keys |
| Consistent Hashing | Hash ring + virtual nodes | Minimal remapping on add/remove | More complex, virtual nodes needed for uniformity |
| Directory Sharding | Lookup table: key shard | Flexible, can move data | Lookup service = SPOF, extra hop |
| Geo Sharding | US shard1, EU shard2 | Data locality, compliance (GDPR) | Cross-region queries expensive |
Sharding Pitfalls
Cross-shard JOINs = scatter-gather (expensive) Distributed transactions across shards need 2PC Rebalancing requires data migration Choose shard key carefully hard to change later Hot shard = one shard receives disproportionate trafficCaching
Caching Strategies All Patterns
Critical
| Strategy | Flow | Use When | Downside |
|---|---|---|---|
| Cache-Aside (Lazy) | Check cache miss load DB store in cache | Read-heavy, tolerate some stale data | Cache miss penalty, thundering herd on cold start |
| Read-Through | Cache handles DB fetch transparently on miss | Simplified app code, uniform read path | Cache must understand data model |
| Write-Through | Write to cache AND DB synchronously | Strong consistency needed | Write latency doubled |
| Write-Behind (Write-Back) | Write to cache; async flush to DB | Write-heavy, latency-critical | Data loss if cache crashes before flush |
| Write-Around | Write directly to DB, skip cache | Write-once, read-rarely data (logs) | Cache miss on first read after write |
| Refresh-Ahead | Proactively refresh cache before TTL expires | Predictable, hot data access patterns | Wasted refresh if data not re-accessed |
Eviction Policies
- LRU (Least Recently Used): evict least recently accessed. Best for most workloads.
- LFU (Least Frequently Used): evict least accessed overall. Good when some items are permanently hot.
- TTL (Time-To-Live): expire after fixed duration. Prevents stale data.
- FIFO: simple but poor hit rate. Rarely used in production.
Redis Data Structures for System Design
String: counter (INCR), session, feature flag, simple KVHash: user profile fields (HSET/HGET), object propertiesList: activity feed, queue (LPUSH/RPOP), recent N itemsSet: unique visitors, followers, tags (SADD/SINTER for mutual friends)Sorted Set (ZSet): leaderboard (ZADD/ZRANGE), rate limiting, priority queuePub/Sub: real-time notifications, chat cross-server broadcastStreams: durable event log (lightweight Kafka replacement)Geo: GEOADD/GEORADIUS for nearest driver, nearby stores
Cache Problems & Solutions
| Problem | Cause | Solution |
|---|---|---|
| Cache Stampede (Thundering Herd) | Many concurrent misses hit DB simultaneously after cache expiry | Mutex/distributed lock, probabilistic early expiry, background refresh, never-expire + async update |
| Cache Penetration | Repeated queries for non-existent keys bypass cache, hammer DB | Cache null values with short TTL; Bloom Filter to pre-check existence |
| Cache Avalanche | Mass cache expiry at same time DB overwhelmed | Randomize TTL (base_ttl + random(300s)); circuit breaker; multi-layer cache |
| Hot Key Problem | Single key (celebrity post) gets extreme traffic on one shard | Replicate hot key to multiple shards; local in-process L1 cache; rate limit |
Microservices & Architecture Patterns
Monolith vs Microservices
Monolith Pros
- Simple to develop, test, deploy
- No distributed system complexity
- No network latency between components
- ACID transactions are trivial
- Single deployable artifact
Monolith Cons
- Must scale entire app even if only one part needs it
- One tech stack for everything
- Large codebase = slow builds, hard cognitive load
- Deploy all-or-nothing risky releases
Microservices Pros
- Independent deploy, scale, and fault isolation
- Technology flexibility per service
- Small teams own individual services (Conway's Law)
- One service crash doesn't bring down the whole system
Microservices Cons
- Distributed systems complexity (latency, partial failures)
- Cross-service transactions require Saga / 2PC
- More infrastructure: service discovery, API gateway, tracing
- Debugging is harder; need distributed tracing (Jaeger, Zipkin)
When to Choose Microservices
When different parts have drastically different scaling needs, different teams need independent release cycles, or different components need different technology stacks. NOT for early-stage premature optimization kills velocity.API Gateway Pattern
API Gateway Pattern
Mobile App
Web App
Partner API
β
API GatewayAuth Β· Rate Limit Β· SSL Β· Route Β· Trace Β· Transform
β routes to microservices
User Service:8001
Order Service:8002
Catalog Service:8003
Search Service:8004
Client (Mobile / Web / Third-party)
API Gateway - Auth (JWT validation, one place) - Rate Limiting (per client / IP) - SSL Termination - Request Routing / Load Balancing - Request / Response Transformation - Logging, Distributed Tracing - Circuit Breaking User Service Order Service Catalog Service
Tools
Kong, AWS API Gateway, NGINX, Traefik, Envoy, Spring Cloud GatewayBFF Pattern (Backend for Frontend)
- Separate API gateway per client type (mobile BFF, web BFF)
- Each BFF aggregates exactly the data that client needs
- Avoids over-fetching on mobile (bandwidth matters) vs web
CQRS Command Query Responsibility Segregation
Architecture Pattern
Commands (writes) Queries (reads)
Command Handler Query Handler
Write Model Read Model
(normalized SQL) (denormalized view, Elasticsearch, Redis)
Events published Kafka Consumers update read model async
Example: Orders
POST /orders Write to MySQL publish OrderPlaced Kafka
Kafka consumer build summary view in Redis / Elasticsearch
GET /orders/summary reads from fast read store
Pros
- Optimize reads and writes independently
- Scale read and write services separately
- Read model tailored per use case
- Natural fit with Event Sourcing
Cons
- Eventual consistency between read/write models
- Significantly more complex architecture
- Duplicate data, sync must be maintained
- Overkill for simple CRUD apps
Event Sourcing
Architecture Pattern
Core Idea: Store sequence of events, not current state
- Every state change = immutable event appended to event log
- Current state = replay all events from the beginning
- Like Git for your data full history, time travel, audit trail
- Snapshots: periodically save state to avoid full replay from the beginning
Traditional: Event Sourcing:
Account { balance: $150 } Event Log (append-only):
1. AccountOpened {balance: $0}
2. Deposited {amount: $200}
3. Withdrawn {amount: $50}
Current state: replay all $0 + $200 - $50 = $150
Relevant to Alation
Data lineage, audit trail, metadata versioning these are core Alation use cases where event sourcing shines. Know this well.Saga Pattern Distributed Transactions
Distributed
Problem
How do you maintain consistency across multiple microservices without a distributed ACID transaction (2PC)?Two Saga Types
- Choreography: Services react to domain events. No central coordinator. Decoupled but hard to visualize flow.
- Orchestration: Central Saga Orchestrator sends commands to each service and handles failures. Easier to trace, single place for flow logic.
Choreography (Event-driven):
OrderPlaced InventoryReserved PaymentProcessed Shipped
(failure)
PaymentFailed InventoryReleased OrderCancelled
Orchestration:
SagaOrchestrator reserve-inventory process-payment ship done
(on any failure) compensate backwards
Outbox Pattern Reliable Event Publishing
Problem
If you write to DB and then publish to Kafka, what if Kafka publish fails? You have inconsistent state.Solution: Transactional Outbox
DB Transaction (atomic): 1. INSERT INTO orders ... 2. INSERT INTO outbox_events ... Outbox Relay (Debezium CDC or polling):
Kafka downstream consumers
How It Works
- Write business record AND outbox event in same DB transaction (atomic)
- Separate relay process reads outbox table and publishes to Kafka
- Relay uses CDC (Debezium reads WAL) or polling loop
- Guarantees at-least-once delivery; consumer must be idempotent
Resilience Patterns
Circuit Breaker Pattern
Must Know
Problem
If Service B is slow, calls from A to B pile up, threads exhaust, A also fails cascading failure across the system.Circuit Breaker State Machine
CLOSEDAll requests pass through
β failures > 50% β
OPENFail fast β no calls sent
β 30s timeout β
HALF-OPEN1β2 probe requests allowed
Normal operation
Returns cached fallback
SuccessβCLOSED | FailβOPEN
CLOSED (normal) OPEN (failing) HALF-OPEN (testing)
All requests flow fail fast (no requests) allow 1-2 test requests
failure rate timeout expires fail OPEN succeed CLOSED
exceeds threshold OPEN
Configuration
- Failure threshold: 50% failure rate in 10s window OPEN
- Open duration: 30s (fail fast, don't retry hammering broken service)
- Success threshold to close: 3 consecutive successes in HALF-OPEN
- Fallback: return cached response, default value, or degrade gracefully
- Libraries: Resilience4j (Java), Hystrix (Netflix, deprecated), Polly (.NET)
Retry, Timeout, Bulkhead, Rate Limiting
| Pattern | What It Does | Key Config |
|---|---|---|
| Retry + Backoff | Re-attempt failed requests with exponential backoff + jitter | max_retries=3, delay=2^n * 100ms + random(100ms) |
| Timeout | Fail fast if response not received in time limit | connect_timeout=3s, read_timeout=10s |
| Bulkhead | Isolate thread/connection pools per downstream service | maxConcurrentCalls=10 per downstream |
| Rate Limiting | Limit incoming requests per client/IP/API key | 100 req/min per API key |
| Fallback | Return degraded response instead of error | Return cached data, empty list, default value |
Exponential Backoff + Jitter Formula
delay = min(cap, base * 2^attempt) + random(0, jitter_max). Without jitter all retriers retry simultaneously creates another thundering herd on the recovering service.Rate Limiting Algorithms
| Algorithm | How | Burst | Memory | Notes |
|---|---|---|---|---|
| Token Bucket | Bucket refills at rate R, each request uses 1 token | Yes (burst up to bucket size) | O(1) | Most common; allows burst |
| Leaky Bucket | Queue requests, process at fixed rate | No (smooth output) | O(queue) | Smooth traffic shaping |
| Fixed Window | Count per fixed minute/hour window | Yes (edge burst issue) | O(1) | Simple; 2x burst possible at boundary |
| Sliding Window Log | Store timestamp per request, count within window | Accurate | O(requests) | Accurate but memory-intensive |
| Sliding Window Counter | Blend current + previous window proportionally | Approximate | O(1) | Good balance; Redis-friendly |
Redis Rate Limiting (Sliding Window Counter)
- Key:
rate:{user_id}:{current_minute} - INCR + EXPIRE (atomic with Lua or pipeline)
- Blend: count = current_bucket + previous_bucket * (1 - elapsed/window)
Distributed Systems Concepts
Consistent Hashing
Hash Ring (0 2^32 - 1):
0 (=2^32)
S1 (@ 100)
350 200
S4 [ring] S2
300
S3 (@ 300)
Key lookup: hash(key) % 2^32 walk clockwise to nearest server
Adding S5 between S1-S2: only keys in [hash(S1), hash(S5)] remap
Removing S2: only S2's keys move to S3
Virtual Nodes: S1 maps to 150 positions on ring (S1-vn1, S1-vn2 ...)
Much more uniform distribution even with heterogeneous hardware
Used In
- Cassandra, DynamoDB: partition data across nodes
- Redis Cluster: slot assignment (actually uses fixed 16384 slots)
- CDNs: route requests to nearest/least-loaded edge node
- Load balancers: session affinity without lookup tables
Distributed Locking
| Approach | How | Trade-offs |
|---|---|---|
| Redis SET NX EX | SET lock_key owner NX EX 30 atomic set-if-not-exists with TTL | Fast, simple; Redlock algorithm for multi-node safety |
| Zookeeper | Create ephemeral sequential znodes; lowest sequence = lock holder | Strongly consistent; handles crashes via ephemeral nodes |
| DB Pessimistic Lock | SELECT ... FOR UPDATE on DB row | Simple; slow; doesn't work cross-service |
| Optimistic Locking | version column; UPDATE WHERE version=expected; retry on conflict | No blocking; great for low contention; retry logic needed |
Redis Lock Safety Rule
Only release the lock if YOU own it: check owner == your_id before DEL. Use Lua script for atomic check-and-delete. Auto-expiry prevents deadlock if holder crashes.Load Balancing Algorithms
| Algorithm | How | Best For |
|---|---|---|
| Round Robin | Rotate through servers in order | Uniform servers, stateless requests |
| Weighted Round Robin | Proportional to server capacity | Heterogeneous hardware |
| Least Connections | Route to server with fewest active connections | Long-lived connections, variable request duration |
| IP Hash | hash(client_ip) % N same client same server | Session stickiness (stateful apps) |
| Least Response Time | Lowest latency + fewest connections combined | Latency-sensitive APIs |
L4 vs L7 Load Balancing
- L4 (Transport): routes by IP + TCP/UDP port. Very fast, no HTTP inspection. AWS NLB.
- L7 (Application): routes by HTTP path, headers, cookies. Smarter. /api API servers; /static CDN. AWS ALB, NGINX.
Bloom Filters
Probabilistic data structure: "definitely NOT in set" OR "probably in set"
- Space-efficient: represents 1M items in ~1MB vs hash set in 100MB
- False positives possible, false negatives never
- Cannot delete (use Counting Bloom Filter for deletion)
| Use Case | How Bloom Filter Helps |
|---|---|
| URL Shortener | Check if short code exists before DB query |
| Cache Penetration | Block queries for non-existent keys before they hit DB |
| Email deduplication | Check if email already sent before DB lookup |
| Chrome Safe Browsing | Quick check if URL is malicious |
| Cassandra read path | Check if key exists in SSTable before disk read |
Networking & Communication
REST vs GraphQL vs gRPC
API Design
| Factor | REST | GraphQL | gRPC |
|---|---|---|---|
| Protocol | HTTP/1.1 + JSON | HTTP/1.1 + JSON | HTTP/2 + Protobuf |
| Schema | OpenAPI (optional) | Strongly typed SDL | Strongly typed .proto |
| Over/Under-fetch | Common problem | Client specifies exact fields needed | N/A (binary, compact) |
| Performance | Good | Good (1 round trip for complex data) | Excellent (binary, HTTP/2 multiplexing) |
| Streaming | SSE / WebSockets | Subscriptions (WebSocket) | Bidirectional streaming native |
| Use case | Public APIs, simple CRUD | Mobile BFF, complex nested queries | Internal microservices, high-throughput |
| Caching | Easy (HTTP cache headers) | Complex (POST-based, no HTTP cache) | Custom only |
| Tooling | Excellent (Postman, curl) | Good (GraphiQL, Apollo) | Good (grpcurl, Buf) |
WebSockets vs Long Polling vs SSE
| Approach | How | Latency | Direction | Use Case |
|---|---|---|---|---|
| Short Polling | Client requests every N seconds | Up to N seconds | Pull only | Simple updates where delay is OK |
| Long Polling | Server holds request open until data available | Low | Pull only | Simple push before WebSockets era |
| SSE | Server pushes over persistent HTTP (text/event-stream) | Very low | Server Client only | Dashboards, live feeds, notifications |
| WebSockets | Full-duplex TCP after HTTP upgrade handshake | Very low | Bidirectional | Chat, gaming, collaborative editors |
WebSocket at Scale Challenge
WebSocket servers are stateful (connection on specific server). Solution: Sticky sessions at LB + Redis Pub/Sub to broadcast messages across servers. Any server can publish to Redis; subscriber servers push to their connected users.CDN & DNS
CDN Flow:
User (India) DNS CDN Edge Node (Singapore nearest)
Cache Hit return immediately
Cache Miss fetch from Origin (US) cache return
DNS Resolution:
Browser OS Cache DNS Resolver (ISP/8.8.8.8)
Root NS (".") TLD NS (".com") Authoritative NS
Returns IP Browser connects (TTL cached)
CDN Cache Invalidation
- Versioned URLs (best practice): /app.v3.2.1.js new URL = new cache entry
- TTL-based: Cache-Control: max-age=86400
- Purge API: Explicitly invalidate specific paths on deploy (Cloudflare, Fastly)
Reverse Proxy vs Forward Proxy
- Forward Proxy: Client-controlled. Client Proxy Internet. VPN, content filtering.
- Reverse Proxy: Server-controlled. Internet Proxy Servers. Load balancing, SSL termination, hiding server topology.
Authentication & Security
JWT vs Session Tokens
JWT (Stateless):
Header.Payload.Signature
Header: {"alg":"HS256","typ":"JWT"}
Payload: {"sub":"user123","roles":["admin"],"exp":1234567890}
Signature: HMAC-SHA256(base64(header)+"."+base64(payload), secret)
Server validates signature ONLY no DB lookup
Cannot revoke before expiry (use short TTL: 15min)
Refresh token (long-lived, DB-backed) to get new access token
Session (Stateful):
Client sends: Cookie: session_id=abc123
Server: SELECT * FROM sessions WHERE id='abc123' (Redis for speed)
Can revoke instantly (delete from sessions table)
| JWT | Session Token | |
|---|---|---|
| Scalability | Excellent no shared state | Needs shared Redis/DB across all servers |
| Revocation | Hard need token blacklist in Redis | Easy delete session record |
| Token Size | Large (~500 bytes) | Small (random 32-byte string) |
| Best For | Microservices, stateless APIs, mobile | Traditional web apps, high-security (banking) |
OAuth 2.0 Authorization Code Flow
1. User clicks "Login with Google"
2. App redirects: GET /oauth/authorize
?client_id=APP_ID
&redirect_uri=https://app.com/callback
&scope=email+profile
&response_type=code
&state=random_csrf_token <-- CSRF protection
3. User authenticates & grants permission at Google
4. Google redirects: GET https://app.com/callback
?code=AUTH_CODE
&state=random_csrf_token <-- verify state matches
5. App backend calls: POST /oauth/token
{code: AUTH_CODE, client_secret: SECRET, grant_type: authorization_code}
6. Google returns: {access_token, refresh_token, expires_in}
7. App calls: GET /userinfo with Bearer access_token
8. On expiry: POST /oauth/token {grant_type: refresh_token, refresh_token: RT}
Key Security Points
state param = CSRF protection PKCE replaces client_secret for mobile/SPA access_token = short-lived (15min) refresh_token = long-lived (30 days, stored securely server-side) Never expose client_secret in frontendSystem Design Interview Questions
Interview Framework
- Clarify Requirements β functional + non-functional, scale
- Estimate Scale β RPS, storage, bandwidth
- High-Level Design β draw major components
- API Design β endpoints, data models
- Deep Dive β critical components & bottlenecks
- Trade-offs β what you chose and why
Q1
Design a URL Shortener (TinyURL)
Requirements: Shorten URLs, redirect, analytics, expiry. Write: 100M URLs/day. Read: 1B redirects/day (10x writes).
Estimates: 1B reads/day = 12K RPS. 100M writes/day = 1.2K RPS. Storage: 100 bytes * 100M * 365 days * 5 years = ~18TB.
Short Code: Base62 (a-z, A-Z, 0-9). 7 chars = 62^7 = 3.5 trillion unique codes. Use DB auto-increment ID convert to Base62.
URL Shortener Architecture
WRITE PATH β 1.2K RPS
Client
POST /shorten β
Load Balancer
β
Shortener Service
β
MySQLid β short_code
READ / REDIRECT PATH β 12K RPS
Client
GET /abc123 β
Redirect Service
β
Redis CacheHIT: 302 redirect
MISS β
MySQLlookup + cache fill
ANALYTICS β ASYNC
Redirect Service
β
Kafkaclick events
β
Analytics Consumer
β
ClickHouseaggregates
Write: POST /shorten {long_url, expiry}
ShortenerService INSERT into MySQL (id, short_code, long_url, expiry)
Return https://short.ly/abc1234
Read: GET /abc1234
Redis cache lookup (short_code long_url)
HIT: 302 Redirect long_url
MISS: MySQL lookup populate Redis (TTL 24h) 302 Redirect
Analytics (async):
On each redirect Kafka event {short_code, timestamp, ip, user_agent}
Analytics Consumer ClickHouse / DynamoDB for aggregates
Design Decisions
- Auto-increment + Base62: no collision, simple
- Redis cache: 99%+ redirects served from cache
- 302 redirect (not 301): server sees every request accurate analytics
- Async analytics: redirect latency unaffected
Trade-offs
- Auto-increment is guessable (sequential codes)
- Hash-based codes: possible collision need retry logic
- Pre-generated codes pool: batch-generate offline
- Bloom filter: check code non-existence before DB query
Q2
Design a Chat System (WhatsApp / Slack)
Requirements: 1:1 and group chat, online presence, message history, delivery/read receipts, media sharing.
Real-time Transport: WebSocket (full-duplex, bidirectional). Long Polling as fallback for restrictive networks.
Message Storage: Cassandra. RowKey = channel_id, clustering by timestamp. Write-heavy, append-only, time-ordered = perfect fit.
Chat System β Real-time Message Flow
AliceWebSocket
β msg β
Chat Server AAlice's connection
PUBLISH β
Redis Pub/Subchannel: user:bob
β DELIVER
Chat Server BBob's connection
β push β
BobWebSocket
β async persist
Kafkamessages topic
β
Cassandramsg history by channel
Presence Service
heartbeat β
Redisuser:online:{id} TTL 60s
Message Flow (Alice Bob):
1. Alice sends msg via WebSocket Chat Server A
2. Chat Server A:
a) Persist to Cassandra async
b) PUBLISH to Redis channel "user:bob:msgs"
3. Chat Server B (Bob's connection) subscribes receives from Redis
4. Chat Server B delivers to Bob via WebSocket
5. Bob's client sends "delivered" ack Chat Server B Cassandra
6. Alice receives delivery receipt via WebSocket
Group Chat (fan-out):
message Kafka Fan-out consumer deliver to each member's inbox
For large groups (10K+): pull model members fetch on open
Presence Service:
WebSocket connect SET user:online:{userId} 1 EX 60 (heartbeat every 30s)
Heartbeat miss key expires user offline
Key Decisions
- Redis Pub/Sub connects stateful WebSocket servers
- Cassandra: ideal for high-write time-series message history
- Offline inbox: store in DB, deliver on reconnect
- Media: upload to S3, share URL in message
Challenges
- Large groups (100K): fan-out on write too expensive use pull
- Message ordering: Cassandra stores by timestamp; tie-break with UUID
- E2E encryption: client encrypts, server stores ciphertext only
- Message dedup: idempotency key per message to prevent duplicates on retry
Q3
Design Twitter / News Feed
Core Challenge: Fan-out on Write vs Fan-out on Read
- Push (Fan-out on Write): When user tweets, push to all followers' Redis feeds. Fast reads. Bad for celebrities (50M followers).
- Pull (Fan-out on Read): Merge tweets from followed users at read time. Slow reads. DB expensive.
- Hybrid (Twitter's approach): Push for regular users, Pull for celebrities at read time. Best of both.
Twitter β Hybrid Fan-out Architecture
WRITE: User posts tweet
User (1K followers)
POST /tweet β
Tweet Service
β
Cassandratweets by userId
Kafkatweet-created
β for <10K followers β
Fan-out Worker
β
Redis ZSetfeed:{followerId}
READ: User loads home feed
Feed Service
β
Redis ZSetregular tweets (pre-built)
+
Celebrity pullfetch top 20 directly
β merge β
Feed
Tweet Write:
POST /tweet TweetService Cassandra (tweets by userId + timestamp)
Kafka: "tweet-created" event
FanoutWorker (async):
for each follower (if follower_count < 10K):
ZADD feed:{followerId} tweet.timestamp tweet.id
(celebrities skip pulled at read time)
Feed Read:
GET /feed/{userId}
Feed Service:
1. ZRANGE feed:{userId} 0 20 get regular user tweet IDs
2. For each celebrity followed: fetch their latest 20 tweets directly
3. Merge all sort by timestamp return top 20
Like/RT:
Redis: INCR likes:{tweetId}
Async flush to Cassandra every 5min
Architecture Decisions
- Cassandra for tweets: write-heavy, time-series pattern
- Redis Sorted Set for feed: O(log N) insert, O(1) range read
- Async fan-out via Kafka: tweet API returns instantly
- S3 + CDN for media content
Trade-offs
- Push model: more storage (each tweet copied to N followers' feeds)
- Pull model: slow read (query N followed users, merge, sort)
- Celebrity tweets: fan-out to 100M followers takes minutes
- Trending: sliding window count via Redis sorted sets
Q4
Design YouTube / Video Streaming
Upload: Chunked upload to S3 Kafka event Transcoder cluster (FFmpeg) multiple resolutions (360p/720p/1080p/4K) output to CDN
Streaming: HLS (HTTP Live Streaming). Video split into ~10s segments (.ts files). Manifest file (.m3u8) lists URLs. Client adapts resolution based on bandwidth (ABR).
Metadata: MySQL for structured data (title, description, tags, owner). Redis for view counts. Elasticsearch for search.
Upload Flow:
Client Chunked multipart upload (resumable) S3 (raw)
S3 event Kafka: "video-uploaded"
Transcoder workers (GPU instances):
Input: raw.mp4 FFmpeg [360p, 720p, 1080p, 4K] + thumbnails
Upload outputs CDN origin CDN distributes globally
Metadata service: UPDATE videos SET status='published' WHERE id=...
Playback:
GET /video/{id}/manifest return .m3u8 (HLS manifest file)
Client parses fetches segments from CDN edge
Client detects bandwidth drop switches to lower quality segment URL
Key Decisions
- CDN essential: video = 80%+ of internet traffic
- HLS/DASH: industry standard, all browsers support
- Async transcoding: instant upload response, background processing
- Separate read/write paths (CQRS-like for metadata)
Challenges
- Storage: 1 video 5 qualities + thumbnail + captions = 10x raw size
- Transcoding cost: GPU instances expensive; queue management critical
- View count: Redis INCR async flush to DB (approximate but fast)
- Copyright: Content-ID fingerprinting pipeline runs in background
Q5
Design a Distributed Cache (like Redis Cluster)
Data Distribution: Consistent hashing. hash(key) maps key to server. Virtual nodes for uniform distribution.
Replication: 1 primary + 2 replicas per shard. Reads go to replicas (eventual consistency). Writes always to primary.
Eviction: LRU per shard. Track access time. When memory full, evict least-recently-used entries first.
Persistence: RDB (point-in-time snapshot, compact) + AOF (append-only log, durable). Use both for full safety.
Design Decisions
- In-memory: microsecond latency vs millisecond for disk
- Single-threaded event loop: no lock contention
- Pipelining: batch multiple commands in one round-trip
- Lua scripting: atomic multi-command operations
Trade-offs
- Memory limit: working set must fit in RAM (expensive)
- Hot key: shard replicas, local L1 cache in application
- Replication lag: replica may briefly serve stale reads
- Split-brain: use Redis Sentinel or Cluster for HA failover
Q6
Design Search Autocomplete (Typeahead)
Requirements: Top-5 suggestions per prefix, under 100ms latency. 10M users, 10 queries each = 100M queries/day = ~1,200 RPS.
Data Structure: Trie with frequency at each node. Or Elasticsearch prefix query with score boosting.
Ranking: Query frequency + recency + personalization weight.
Query (read, latency-critical):
GET /autocomplete?q=data+cat
CDN (cache popular prefixes like "data", "date", "da")
Redis: ZREVRANGE prefix:data_cat 0 4 [top5 sorted by score]
Trie Service (in-memory, sharded by first char)
Elasticsearch fallback
Update (async):
User search logs Kafka Aggregation Service (5-min windows)
Update term frequency Rebuild top-K per prefix
Push to Redis ZSet (ZADD prefix:data_cat freq "data catalog")
Optimizations
- Trie sharded by first 2 chars (26^2 = 676 shards)
- Redis ZSet caches top-5 per prefix (1-hour TTL)
- CDN caches most common prefixes globally
- Client debounce: only query after 300ms idle
Challenges
- Trie memory: 5M terms avg 8 chars = large in-memory footprint
- Real-time updates to trie are expensive batch updates every 5 min
- Personalization: blend global score + user history with weights
- Multi-language: Unicode = more complex trie / different sharding
Q7
Design a Notification System
Event Sources:
User Action / Business Alert / Marketing Campaign
Notification Service API
(validates payload, applies user preferences, deduplication check)
async
Kafka Topics (separate topics per channel):
-- notifications-email Email Worker SendGrid / SES (retry + DLQ)
-- notifications-sms SMS Worker Twilio (retry + DLQ)
-- notifications-push Push Worker FCM / APNs (retry + DLQ)
-- notifications-inapp InApp Worker WebSocket / DB
Notification DB:
-- Delivery status (sent, delivered, failed, read)
-- User preferences (email opt-in, push enabled, quiet hours 10pm-8am)
Key Decisions
- Idempotency key: prevents duplicate sends across retries
- User preferences service: opt-outs, quiet hours, frequency caps
- Priority lanes: critical alerts bypass normal queue
- Rate limit: max 3 emails/hour per user to prevent spam
Challenges
- Deduplication: same event published twice idempotency key check before send
- Marketing blast to 10M: partition workers, throttle to ~100K/min
- Delivery tracking: webhooks from SendGrid/Twilio update status
- DLQ monitoring: alert on-call when DLQ grows beyond threshold
Q8
Design Uber / Ride-Hailing
Location Tracking: Drivers send GPS every 5s. GEOADD in Redis (O(log N) insert). GEORADIUS for nearest-driver query.
Geohashing: Encode lat/lng as a string. Nearby locations share prefix. Efficient area queries without lat/lng math.
Matching: Find N nearest available drivers send offer simultaneously first accept wins create Trip (MySQL, ACID).
Driver Location:
Driver app WebSocket Location Service GEOADD drivers {lng, lat, driver_id}
Ride Request:
User RideRequest Service:
1. GEORADIUS(user_lat, user_lng, radius=5km) [driverA, driverB, driverC]
2. Filter: available + rating > 4.5 + correct vehicle type
3. Offer sent to top 3 simultaneously (first accept wins)
4. Accepted: INSERT INTO trips (atomic, MySQL) lock driver state
Real-time Trip Tracking:
Driver GPS every 3s Kafka TripTrackingService WebSocket User
Key Decisions
- Redis Geo: O(log N) radius search, in-memory speed
- MySQL for trips: ACID needed for payment integrity
- Cassandra for location history: write-heavy time-series
- Surge pricing: pre-computed by geohash zone demand
Challenges
- Simultaneous accept: optimistic lock on driver status (available in-trip)
- GPS accuracy: Kalman filter to smooth noisy GPS readings
- Driver state machine: available offered in-ride available
- ETA: integrate OSRM / Google Maps API for routing and ETA
Q9
Design a Data Catalog (like Alation)
Core Features: Metadata ingestion from databases/BI tools, search, lineage tracking, data quality scoring, collaboration (stewardship, annotations).
Metadata Storage: Graph DB (Neo4j) for lineage (A B C relationships). Elasticsearch for full-text search. PostgreSQL for structured catalog data.
Connectors: Pull metadata via JDBC/REST from source systems (Snowflake, BigQuery, Tableau). Schedule crawls. Detect schema drift.
Metadata Ingestion:
Connector Framework Source System (Snowflake/BigQuery/Tableau)
Extract: tables, columns, types, stats, query history
Transform: normalize to internal metadata model
Load:
PostgreSQL (structured catalog: tables, columns, owners)
Elasticsearch (search index: name, description, tags)
Neo4j (lineage graph: table derived from upstream table)
Event Store (schema change history for audit)
Search:
User query Elasticsearch (full-text + faceted filtering)
Boost: verified assets, frequently accessed, recently updated
Lineage:
MATCH (a)-[:DERIVED_FROM*]->(b) WHERE a.name='revenue_report'
Show upstream impact analysis for data quality issues
Key Decisions
- Graph DB: lineage relationships are inherently graph-structured
- Elasticsearch: fuzzy search, facets, ranking by relevance
- Event Sourcing: full audit trail of metadata changes
- CQRS: write-optimized ingest path, read-optimized search path
Challenges
- Schema drift: source changes must detect and re-index affected assets
- Scale: 100K+ tables, millions of columns = large search index
- Trust/quality scoring: aggregate from multiple signals (freshness, usage, docs)
- Real-time lineage: parse SQL query logs to extract column-level lineage
Looking for HLD Questions?
Big-infrastructure designs (Google Docs, Maps, Netflix, Drive, Payments, Instagram and more) are in the High-Level Design (HLD) β page.
HLD-1
Design Google Docs (Collaborative Document Editor)
Requirements: Multiple users edit same document simultaneously in real-time. Changes visible to all within <100ms. Support text, formatting, comments, version history, offline editing, cursor presence.
Scale: 1B documents, 100M DAU, up to 100 concurrent editors per doc. 99.99% availability.
Core Challenge: Conflict resolution when concurrent edits happen. How do two users editing position 5 simultaneously converge to the same document?
Architecture:
Client (Browser / Mobile)
Local Document State (shadow copy + pending ops queue)
WebSocket connection to Doc Server (sticky session via consistent hash on docId)
Doc Server (one per active document session):
Receives op from User A: {type:insert, pos:5, char:'X', rev:42, clientId:'A'}
Transforms op against any concurrent ops received (OT algorithm)
Appends to document Op Log (ordered)
Broadcasts transformed op to all connected clients
Persists to Op Storage
Storage:
Op Log: Cassandra (append-only, docId + revision as PK, fast writes)
Document Snapshots: S3 (full doc state every 1000 ops for fast load)
Metadata: MySQL (doc title, owner, ACL, created_at, last_modified)
Redis: active session routing (docId β server instance mapping)
Real-time Sync (OT):
Server serializes all ops (single authority)
Client sends: op + base_revision
Server transforms op against all ops since base_revision
Committed op broadcast with new revision number to all clients
Client algorithm:
Applied locally immediately (optimistic): instant feedback
Receive server ack: adjust revision counter
Receive other user's op: transform against local pending ops
Cursor Presence & Awareness
- Each user's cursor position = op with {type: cursor, pos, userId, color}
- Broadcast via same WebSocket channel but not persisted (ephemeral)
- Throttled: send cursor updates max every 50ms (debounce)
- Display: other cursors rendered as colored carets with username label
Offline Editing & Sync
- Client stores pending ops in IndexedDB while offline
- On reconnect: send all buffered ops with last-known revision
- Server replays and transforms against ops committed while offline
- CRDTs (alternative): Automerge/Yjs β no server-side transformation needed, peer-to-peer merge
Key Design Decisions
- OT with central server: battle-tested, Google's actual approach
- Cassandra for op log: append-only + high write throughput
- Snapshot every N ops: fast doc load without replaying all ops
- WebSocket per doc server: low latency, stateful connection
- Consistent hashing on docId: all clients of same doc β same server
Deep Dive Questions
- How to handle server crash during active session? (Redis TTL + reconnect)
- How to load a 500-page doc quickly? (latest snapshot + delta ops)
- Permission check on every op? (token in WebSocket handshake, server enforces)
- Comments vs inline edits: separate op types, thread anchored to character range
HLD-2
Design Google Maps (Navigation & Geo Service)
Requirements: Search POIs, get directions (car/walk/transit), real-time traffic overlay, ETA, turn-by-turn navigation, map tile rendering, live traffic contribution from users.
Scale: 1B+ users, 25M+ updates/day from users. Map data = petabytes. 99.99% uptime for navigation.
System Components:
1. Map Tile Service:
World map pre-rendered into tiles at 21 zoom levels
Tile naming: zoom/x/y.png (quadtree coordinates)
Stored on Object Storage (GCS) served via CDN globally
Vector tiles (modern): send vector data, render client-side (better scaling, dark mode)
Tile generation pipeline: OSM data + satellite imagery Mapnik/custom renderer β tiles
2. Routing Engine:
Road network = weighted directed graph
Nodes: intersections Edges: road segments with weights (distance, speed_limit, traffic)
Algorithm: Bidirectional Dijkstra (15x faster than one-directional)
For Google-scale: Contraction Hierarchies (CH) precompute shortcuts
Preprocess offline: add shortcut edges bypassing less important nodes
Query: expand upward from src AND downward from dst meeting in middle
Result: routes in <1ms even for continent-scale
3. Real-time Traffic:
Driver location probes β Kafka (every 3s while driving, anonymized)
Traffic aggregation service: compute avg speed per road segment (15s windows)
Traffic overlay: Flink stream processor updates segment weights in Redis
Routing engine reads current edge weights from Redis for ETA
4. ETA Prediction:
Historical + real-time traffic + ML model (time of day, day of week, weather)
Continually recalculate during navigation if user deviates or traffic changes
5. Search (POI):
"Coffee near me" β Elasticsearch geo_distance query + ranking by ratings/distance
Named search "Eiffel Tower" β Knowledge Graph (entity resolution) β coordinates
Geocoding (address β lat/lng): Nominatim / in-house model
Reverse geocoding (lat/lng β address): R-tree spatial index
Key Decisions
- Contraction Hierarchies: routing on 10M+ node graph in <1ms
- Quadtree tiles + CDN: 99%+ tile requests served from edge cache
- Kafka for traffic probes: ingests 100M+ updates/day without loss
- Vector tiles: client rendering, reduced data transfer, theme support
- Segment-level traffic: granular road speed, not just major highways
Deep Dive Questions
- Alternative routes: k-shortest paths, re-weight edges with diversity penalty
- Offline maps: pre-download region tiles + routing graph, sync on WiFi
- Tunnel/GPS loss: dead reckoning (speed + heading extrapolation) + map snapping
- Map updates: OSM community + satellite ML detection β daily tile re-renders
HLD-3
Design Google Drive / Dropbox (File Sync & Storage)
Requirements: Upload/download files, sync across devices, versioning, sharing (link + ACL), deduplication, conflict resolution when same file edited on two devices offline.
Scale: 500M users, 50M DAU, avg 2GB per user = 1 exabyte total. 10M uploads/day.
Upload Flow:
1. Client chunks file into 4MB blocks (content-addressed: SHA256 of block)
2. Client checks: which blocks already exist? POST /api/check_blocks [sha256_list]
3. Server: SELECT sha256 FROM blocks WHERE sha256 IN (...) return existing
4. Client uploads ONLY missing blocks (delta sync β huge bandwidth savings)
PUT /upload/block/{sha256} direct to S3 via pre-signed URL (bypass app servers)
5. Client: POST /api/commit_file {filename, block_list: [sha256s], parent_version}
6. Server: INSERT file record + version in MySQL, notify other devices via SSE/WebSocket
Block Storage (Deduplication):
blocks table: {sha256 (PK), s3_path, size, ref_count}
files table: {file_id, user_id, version_id, block_list (JSON array of sha256s)}
Two users upload same file? Both reference same blocks (storage saved!)
Garbage collection: decrement ref_count on delete; S3 deletion when ref_count=0
File Metadata:
MySQL: file hierarchy (parent_folder_id, name, user_id, permissions)
Redis: active user sessions, recently accessed file metadata (hot cache)
Elasticsearch: full-text filename search across all files
Sync Protocol:
Client maintains local delta log (what changed since last sync)
Server sends: GET /changes?since=cursor (long-poll or WebSocket)
Cursor: opaque token representing sync state (like Dropbox's /list_folder/continue)
On change: server publishes event Kafka notifications-{userId} SSE/WebSocket push
Conflict Resolution:
Two devices edit file while offline, reconnect with divergent versions
Strategy: keep BOTH files: "report.docx" + "report.docx (conflict copy, Device A, 2024-01-15)"
User resolves manually (same as Dropbox behavior β don't attempt auto-merge for binary files)
Key Decisions
- Content-addressed blocks: dedup + only upload changed chunks (delta)
- Pre-signed S3 URLs: offload large uploads directly to S3, not app servers
- MySQL for metadata: ACID needed for file hierarchy + permissions
- Chunked upload: resume interrupted uploads (only re-upload missing chunks)
Deep Dive Questions
- Large file upload (10GB): multipart S3 upload, 5min pre-signed URL expiry
- Version history: store block_list per version, rollback = re-commit old block_list
- Shared folder sync: all members receive change notifications
- Client-side encryption: encrypt before upload; server stores ciphertext only
HLD-4
Design Netflix / Video Streaming Platform
Requirements: Stream video on-demand (200M+ subscribers), personalized recommendations, search, content upload pipeline for studios, global CDN delivery, adaptive bitrate.
Scale: 200M subscribers, 100M+ concurrent streams at peak, 15% of global internet traffic.
Content Upload Pipeline (Studio β Users):
Studio uploads raw 4K master β S3 (raw-content bucket)
S3 event β Kafka "content-uploaded" topic
Transcoding Fleet (GPU instances β EC2 P4d):
Input: raw.mp4
FFmpeg transcodes to: 240p/360p/480p/720p/1080p/4K
Codec: H.264 (compatibility) + HEVC/H.265 (60% smaller, for 4K)
Output: HLS/DASH segments (.ts files, ~6s each) + .m3u8 manifests
Upload segments β CDN Origin (S3 bucket closest to edge)
Metadata: MySQL (title, cast, genre, synopsis, age_rating)
Elasticsearch: title + genre + cast search
Playback (Client β CDN):
Client: GET /api/manifest/{contentId} β returns .m3u8 with CDN URLs
Client requests segments: https://cdn-edge.netflix.com/show123/1080p/seg001.ts
CDN edge (Open Connect Appliance β Netflix's own CDN hardware in ISP PoPs)
Edge cache: 99%+ of popular content served directly from ISP network!
Adaptive Bitrate (ABR): client monitors download speed
If bandwidth drops: switch from 1080p manifest to 720p, seamless mid-stream
Recommendation Engine:
Offline: Spark batch jobs on viewing history β collaborative filtering (ALS matrix factorization)
Near-real-time: Kafka stream β Flink updates user affinity scores
Serving: pre-computed top-N recommendations per user stored in Cassandra
Personalized thumbnails: A/B test different thumbnail images per user profile
Resilience (Netflix Chaos Engineering):
Circuit breakers between all microservices (Hystrix/Resilience4j)
Chaos Monkey: randomly kills production instances (tests resilience)
Multi-region active-active: US-East, US-West, EU, Asia
Fallback: if recommendations unavailable β return editorial "top picks"
Key Decisions
- Open Connect (own CDN in ISPs): eliminates 95%+ of transit costs
- HLS/DASH + ABR: smooth experience across all network conditions
- Cassandra for viewing history: write-heavy time-series, global multi-DC
- Chaos engineering: force resilience by breaking prod intentionally
Deep Dive Questions
- Cold start for new content: pre-load segments to edges before release
- DRM: Widevine (Google) / PlayReady (Microsoft) segment encryption
- Bandwidth estimation for CDN: 200M users Γ avg 5 Mbps = 1 Pbps capacity
- Search: Elasticsearch with ML-reranked results based on user profile
HLD-5
Design a Web Crawler (like Google Search Indexer)
Requirements: Crawl 1B+ web pages, extract links, store content, respect robots.txt, avoid duplicate content, refresh stale pages, handle dynamic JS pages.
Scale: 1B pages, avg 100KB/page = 100TB content. Re-crawl top pages every 24h, others weekly. 10K pages/sec sustained crawl rate.
Architecture:
URL Frontier (Priority Queue):
Stores URLs to crawl, prioritized by:
- PageRank estimate (important pages crawled more often)
- Freshness score (last modified headers, update frequency)
- Domain politeness (max 1 req/sec per domain)
Implementation: Kafka topics per domain + Redis sorted set for priority
Crawl Workers (stateless, horizontally scalable):
Dequeue URL from Frontier
DNS resolve (local cache + upstream)
Fetch page (HTTP GET with User-Agent: Googlebot, respect robots.txt)
Dynamic pages: headless Chrome (Puppeteer) for JS rendering
Extract: all href links + raw HTML content
Publish to Kafka:
"pages-crawled" topic: {url, html, timestamp, status_code}
"links-found" topic: {source_url, [discovered_urls]}
Content Processing Pipeline:
HTML Parser β extract text, metadata, canonical URL
Dedup Service: SHA256(normalized_content) β Bloom Filter β URL Set check
Duplicate? Skip. New? Store in object storage (S3), index in Elasticsearch
URL Deduplication:
Bloom filter (1B URLs, 1% FP rate, ~1.2GB RAM) β fast pre-check
URL Set (Redis / DynamoDB) β authoritative dedup store
URL normalization: lowercase, remove fragment, canonical redirect resolution
robots.txt Compliance:
Fetch and cache robots.txt per domain (TTL 24h)
Before crawling any URL: check allow/disallow rules
Crawl-delay directive respected
Refresh Strategy:
High-priority (news, social): re-crawl every 1h via separate high-frequency queue
Normal pages: adaptive based on historical change rate (3 days if rarely changes)
404 pages: exponential backoff (1d, 4d, 16d), eventually discard URL
Key Decisions
- Kafka per-domain topics: natural domain-rate-limiting
- Bloom filter: O(1) dedup check for 1B URLs in 1GB RAM
- Headless Chrome pool: handle JS-heavy SPAs (React, Vue apps)
- Adaptive refresh: crawl budget allocated by page importance
Deep Dive Questions
- Spider traps: infinite URLs (date params, session IDs) β URL length limit + depth limit
- DNS cache poisoning: validate IPs against known-malicious list
- Politeness: per-domain rate limit, respect crawl-delay, identify as bot
- Distributed URL frontier: shard by domain hash, ensure politeness per shard
HLD-6
Design a Payment System (like Stripe / PayPal)
Requirements: Accept card payments, process transactions, handle refunds, prevent double-charges, financial reconciliation, fraud detection. ACID transactions are non-negotiable.
Non-Functional: Exactly-once payment processing (never charge twice). 99.999% availability. PCI-DSS compliance. Sub-3s payment confirmation.
Payment Flow (Critical Path):
1. Client: POST /payments {amount, currency, payment_method_token, idempotency_key}
2. Payment Service: Check idempotency key (Redis NX) β if exists, return cached result
3. Fraud Detection Service (async check within 200ms):
ML model: card velocity, geo anomaly, device fingerprint β risk score
>0.8 risk: decline. 0.5-0.8: 3DS challenge. <0.5: proceed
4. PSP (Payment Service Provider) call: POST to Stripe/Adyen API
β Tokenized card (PAN never stored on our servers β PCI-DSS scope reduction)
β PSP charges card issuer via card network (Visa/MC rails)
5. On PSP response:
SUCCESS: INSERT INTO ledger (txn_id, amount, status='completed') β ACID
FAILURE: INSERT INTO ledger (txn_id, amount, status='failed')
TIMEOUT: async reconciliation job checks PSP status endpoint
6. Return payment result to client
7. Async: emit PaymentCompleted event β Kafka β Order Service, Email, Analytics
Double-Charge Prevention:
Idempotency key (client-generated UUID) stored in Redis with TTL 24h
PSP-level idempotency: pass our idempotency key in PSP request header
DB: UNIQUE constraint on (idempotency_key)
Ledger (Double-Entry Bookkeeping):
Every financial event creates TWO entries (debit + credit):
DEBIT: customer wallet $-50
CREDIT: merchant wallet $+50 (or escrow pending settlement)
Sum of all ledger entries must always = 0 (invariant checked hourly)
Append-only: never UPDATE ledger rows β only INSERT corrections
Reconciliation:
Nightly job: compare our ledger against PSP settlement report
Mismatches (our-success + PSP-fail): auto-refund and alert
Mismatches (our-fail + PSP-success): charge PSP for reverse or log for manual review
Key Decisions
- Double-entry ledger: financial correctness, easy auditing
- Idempotency at every layer: client β service β PSP
- Tokenization: PAN never stored (PCI-DSS compliance)
- Async fraud detection: 200ms budget, non-blocking on payment path
- MySQL/PostgreSQL: ACID for all financial writes
Deep Dive Questions
- PSP timeout: store as "pending", cron job polls PSP status, update ledger async
- Currency conversion: lock exchange rate at payment time, store in transaction row
- Chargebacks: reverse ledger entries, hold merchant funds in escrow during dispute
- Rate limiting: max 5 payment attempts/hour per card to prevent card testing attacks
HLD-7
Design Instagram / Photo Sharing
Requirements: Upload photos/videos, follow users, home feed, explore/search, stories (24h TTL), likes, comments, DMs, notifications. 1B+ users, 100M photos/day.
Photo Upload:
Client β Upload Service (pre-signed S3 URL) β S3 (original)
S3 event β Image Processing Worker (Kafka):
Resize to: thumbnail (150px), feed (640px), HD (1080px), original
Convert to WebP (30% smaller than JPEG)
Generate perceptual hash (dHash) for duplicate detection
Store all variants in S3, URLs in MySQL
Feed Architecture (Hybrid Fan-out like Twitter):
Follower count < 10K (regular users): Fan-out on write
Post created β FanoutWorker β ZADD feed:{followerId} timestamp postId
Feed stored in Redis Sorted Set per user
Celebrity (>10K followers): Fan-out on read
At read time: fetch latest N posts from celebrity's timeline directly
Merge with user's pre-built feed (client or feed service merges)
Feed Read:
Redis ZRANGE feed:{userId} 0 50 β postIds
Batch fetch post metadata from MySQL/Redis
Batch fetch user metadata (avatar, username) from User Service cache
Return assembled feed
Stories:
Upload same as photos, but metadata: TTL = created_at + 86400s
Redis Sorted Set: stories:{userId} (score = expiry_time)
Background job: ZREMRANGEBYSCORE remove expired stories
Viewer set stored in Redis Set (story:{storyId}:viewers) with same TTL
Explore Feed:
Content-based: user's interest graph β candidate posts β ML ranking
Collaborative filtering: users similar to you liked β show their liked posts
Trending hashtags: sliding window count in Redis
Likes Counter:
Redis INCR likes:{postId} (in-memory, fast)
Async flush to PostgreSQL every 60s (batch update)
On read: check Redis first, fallback to DB
Key Decisions
- Hybrid fan-out: pre-built Redis feed for regular users, lazy pull for celebrities
- WebP + multiple resolutions: 30% bandwidth reduction, fast load
- CDN for all media: images served from edge (global low latency)
- Cassandra for DMs: write-heavy, time-ordered messages per conversation
Deep Dive Questions
- Perceptual hash dedup: same photo re-uploaded β point to same S3 object
- Shadowban: filter posts in feed assembly without notifying user
- Feed with follow/unfollow: update fan-out set membership atomically
- Reels (short video): same as YouTube but max 90s, same HLS pipeline
HLD-8
Design a Distributed Task Scheduler (like Airflow / Temporal)
Requirements: Schedule tasks (one-time + cron), assign to workers, retry on failure, handle worker crashes, task dependencies (DAGs), exactly-once execution, monitor task status, priority queues.
Scale: 100M tasks/day, 10K concurrent workers, tasks must run within 1s of scheduled time.
Architecture:
Scheduler (Leader-elected via Zookeeper/etcd):
Cron expressions β parse next N execution times (pre-compute 5min ahead)
INSERT INTO task_queue (task_id, run_at, status='pending', priority)
Leader heartbeat: if leader dies, Raft election for new scheduler leader
Task Queue:
Tier 1: Redis Sorted Set (score = run_at timestamp)
High-frequency polling by dispatcher (every 100ms)
ZRANGEBYSCORE task_queue -inf NOW LIMIT 0 100 β due tasks
ZREM + atomically claim via Lua script (prevent double-dispatch)
Tier 2: MySQL (durable, for recovery after Redis flush)
Redis populated from MySQL on startup; MySQL is source of truth
Dispatcher:
Polls Redis every 100ms for due tasks
Assigns to available worker via consistent hashing (task_type β worker pool)
Worker heartbeats via Redis key (worker:{id} EX 30s)
On heartbeat miss: task reassigned (at-least-once delivery)
Workers (stateless pool):
Long-poll dispatcher queue or subscribe to Kafka topic per task_type
Execute task logic (idempotent!)
On completion: UPDATE task_executions SET status='done', result=... WHERE id=...
On failure: retry with exponential backoff (max_retries configurable)
Poison pills (always-failing): move to DLQ after max_retries
DAG Dependencies:
task_dependencies table: {task_id, depends_on_task_id}
After task completes: check if any downstream tasks now have all dependencies met
If yes: INSERT downstream tasks into task_queue
Exactly-Once Guarantee:
Lease-based: worker gets lease (30s), must renew; if missed, task released to another worker
Task execution ID: unique per attempt β DB UNIQUE constraint prevents double-insert of result
Idempotent task logic: worker code must handle re-execution gracefully
Key Decisions
- Redis sorted set: O(log N) scheduling, ~10ms dispatch latency
- MySQL as durable backup: recover all pending tasks on Redis restart
- Leader election via etcd: prevents multiple schedulers double-scheduling
- DLQ: poisonous tasks isolated, don't block healthy tasks
Deep Dive Questions
- Time zone handling: store all times in UTC, convert at display layer
- At-most-once (financial jobs): 2PC between scheduler DB and task execution record
- Worker autoscaling: monitor queue depth, scale workers up/down via K8s HPA
- Cascading failures: rate limit task retries, circuit break task_type if too many failures
HLD-9
Design Amazon E-Commerce (Product, Cart, Orders, Inventory)
Requirements: Product catalog, search, cart, checkout, order management, inventory, pricing, recommendations. 300M active users, 1M orders/day, Black Friday 10Γ peak.
Service Decomposition:
Product Catalog Service β MySQL (structured) + Elasticsearch (search) + Redis (hot products)
Cart Service β Redis (session-like, ephemeral, fast writes/reads)
Order Service β MySQL (ACID for order lifecycle)
Inventory Service β MySQL (ACID for stock counts, pessimistic lock for reserve)
Pricing Service β Rules engine + dynamic pricing + Redis cache (TTL 5min)
Recommendation Service β Cassandra (view/purchase history) + ML offline model
Checkout Flow (Distributed Transaction β Saga):
1. Cart Service: validate cart items still available
2. Inventory Service: RESERVE stock (decrement available_count, optimistic lock on version)
If stock conflict: return "out of stock", rollback reservation
3. Payment Service: charge card (idempotency key = orderId)
If payment fails: compensate β Inventory Service RELEASE reservation
4. Order Service: CREATE order record with status='confirmed'
5. Inventory Service: COMMIT reservation (move from reserved β sold)
6. Kafka: OrderConfirmed β Fulfillment, Email, Analytics
Inventory Concurrency (Flash Sales β 10K req/sec for 1 item):
Approach 1: Redis DECR available:{productId} (optimistic, fast)
Check >= 0 before decrement via Lua script (atomic)
Flush to DB periodically and on order commit
Approach 2: DB Pessimistic lock
SELECT stock FOR UPDATE UPDATE stock SET count -= 1 WHERE id=...
Safe but serialized β throughput bottleneck under high concurrency
Product Search:
Elasticsearch index: title, description, brand, category, attributes (faceted)
Relevance: BM25 score + boost by sales rank + personalization re-rank
Facets: filter by price_range, brand, rating, Prime eligibility
Spell correction: fuzziness parameter in ES query
Flash Sale Architecture:
Pre-sale: pre-allocate tokens in Redis (1 token = 1 item reservation right)
User claims token first (lottery / first-come): RPOP flash_tokens:{productId}
Got token: proceed to checkout flow with guaranteed inventory
Prevents overselling + reduces DB load during spike
Key Decisions
- Saga (Choreography): each service publishes/consumes events independently
- Redis for cart: sub-millisecond reads, no ACID needed for cart
- Optimistic locking for inventory: high-concurrency without DB blocking
- Flash sale tokens in Redis: decouple demand spike from DB writes
Deep Dive Questions
- Cart persistence: sync Redis cart to DB on checkout (prevent loss on expiry)
- Price consistency: lock price at cart-add or at checkout? (checkout is correct)
- Returns: saga compensation chain β refund payment + restore inventory
- Multi-warehouse: nearest warehouse with stock for fastest delivery ETA
HLD-10
Design a Global CDN / Content Delivery Network
Requirements: Serve static + dynamic content globally with <50ms latency. Cache 90%+ of requests at edge. Handle 10 Tbps peak traffic. DDoS protection. 99.999% uptime. Support video streaming, file downloads, API acceleration.
Architecture Layers:
1. DNS + Anycast Routing:
Customer's domain CNAME β cdn.example.com
Anycast: same IP announced from all PoPs
User's DNS query routes to geographically nearest PoP automatically
GeoDNS alternative: return different IP based on user's location
2. Edge PoP (Point of Presence):
Physical servers in 200+ cities, co-located in major ISPs/IXPs
Local SSD cache: hot content stored on 100TB NVMe per PoP
Software: Nginx/Varnish as cache layer + Envoy for load balancing
TLS termination at edge (reduce round-trip latency dramatically)
On cache miss: fetch from Regional Mid-tier cache or Origin Shield
3. Tiered Cache Architecture:
Edge (city-level) β Regional Mid-tier (continent) β Origin Shield β Customer Origin
Mid-tier collapses cache misses: 1000 edge misses β 1 request to origin
Origin Shield: single PoP that faces origin (protects origin from traffic)
4. Cache Key & Invalidation:
Default cache key: URL + Vary headers (Accept-Encoding, Accept-Language)
Surrogate keys (cache tags): tag content by type (product:{id})
Purge by tag: invalidate all product:{12345} URLs atomically across all PoPs
CDN propagates purge via gossip protocol to all edge nodes (<1s global)
5. Dynamic Content Acceleration:
TLS connection maintained from edge to origin (TCP connection reuse, no handshake per user)
HTTP/2 multiplexing + TCP optimization (CUBIC congestion control at edge)
Route via private CDN backbone (faster + more reliable than public internet)
Argo Smart Routing (Cloudflare): real-time network monitoring picks fastest path
6. DDoS Mitigation:
Volumetric (L3/L4): BGP blackhole + traffic scrubbing centers absorb floods
Application (L7): rate limiting per IP, WAF rules (block SQLi, bad bots)
Anycast absorbs UDP floods: traffic spread across all PoPs, no single point overwhelmed
Challenge pages: JS challenge / CAPTCHA for suspicious IPs
Key Decisions
- Anycast: automatic failover + DDoS absorption across 200+ PoPs
- Tiered caching: collapse origin requests by 100-1000Γ
- Private backbone: 30-50ms faster than public internet for cross-continent
- Surrogate keys: surgical cache invalidation without full purge
Deep Dive Questions
- Video delivery: range request support (byte-range caching per segment)
- Geo-blocking: edge checks IP geolocation against blocklist before serving
- Hot object stampede: request coalescing (only 1 request to origin for same uncached URL)
- HTTPS everywhere: HSTS preloading, OCSP stapling at edge for fast TLS
HLD-11
Design a Metrics & Monitoring System (like Prometheus + Grafana)
Requirements: Collect metrics from thousands of services, store time-series data, query for dashboards, trigger alerts, scale to 1M+ metrics, 1-second granularity, 90-day retention.
Architecture:
Collection (Push vs Pull):
Pull model (Prometheus): scrape /metrics endpoint of each service every 15s
Pros: service doesn't need to know about monitoring infra
Cons: requires service discovery, misses very short-lived processes
Push model (StatsD, CloudWatch Agent):
Service pushes metrics to aggregation agent
Better for serverless/ephemeral functions
Ingestion Pipeline (1M metrics/sec):
Services β Kafka (metrics-raw topic, partitioned by service_name)
Stream processor (Flink): aggregate raw points β 1-min rollups β 5-min rollups
Write to TSDB (Time-Series DB)
Storage β Time-Series Database:
Data shape: (metric_name, labels, timestamp, value)
E.g.: {name: "http_requests_total", labels: {service:"api", status:"200"}, ts: 1715000000, val: 1234.5}
Options:
Prometheus TSDB: local disk, chunks stored as compressed delta encoding
InfluxDB: purpose-built, tag-based indexing, Flux query language
Thanos / Cortex (Prometheus at scale): add S3 for long-term storage, global query
Compression trick: store delta-of-deltas for timestamps + XOR compression for values
Gorilla encoding (Facebook): 1.37 bytes/data point (vs 16 bytes raw)
Query Layer:
PromQL: rate(http_requests_total{status="500"}[5m]) β 5-min error rate
Pre-computed rollups: 1s β 1min β 5min β 1hr aggregates for fast dashboard queries
Cache query results in Redis (30s TTL) for popular dashboard queries
Alerting:
Alertmanager: evaluate alert rules every 15s against recent metrics
Alert: http_error_rate > 0.01 for 5m β fire PagerDuty/Slack
Deduplication: group related alerts, suppress flapping (pending state buffer)
On-call routing: team owns service β route to that team's PagerDuty schedule
Key Decisions
- Kafka buffer: handles ingest spikes without dropping metrics
- Pre-aggregated rollups: fast dashboard queries without scanning raw data
- Gorilla compression: 12Γ storage reduction for time-series
- S3 long-term: cheap historical data, query via Thanos/Athena
Deep Dive Questions
- High cardinality labels (userId as label): cardinality explosion, avoid in metrics (use logs instead)
- Cross-DC aggregation: Thanos sidecar merges metrics across regions globally
- Anomaly detection: seasonal decomposition + z-score alerting (ML-based)
- Retention tiers: 15s granularity 7 days, 1min granularity 30 days, 1hr 1 year