Module 13 · Cloud & System Design · Drills
Reading the framework is not the same as running it under pressure. Work each drill out loud — pretend an interviewer is listening — then reveal the solution and compare your reasoning, not just your answer.
Drill 1 estimation
A photo-sharing app has 10 million daily active users, each uploading 2 photos/day at 3 MB each. Estimate the write QPS and the daily storage growth.
# Writes per day uploads/day = 10,000,000 * 2 = 20,000,000 / day write QPS = 20,000,000 / 100,000s = 200 QPS peak QPS ~ 200 * 3 ~ 600 QPS # Storage per day storage/day = 20,000,000 * 3 MB = 60,000,000 MB = 60 TB / day # -> ~22 PB/year. Object storage (S3), not a database.
Use 100,000 seconds/day and round freely — the interviewer wants the order of magnitude (hundreds of QPS, tens of TB/day), not a precise figure.
Drill 2 SQL / NoSQL / vector
Pick the right store for each, and say why in one line: (a) a banking ledger, (b) a high-write IoT event firehose with flexible fields, (c) "find documents semantically similar to this question."
(a) Banking ledger -> SQL (Postgres)
transactions, strong consistency, ACID — never lose/double money.
(b) IoT firehose -> NoSQL (wide-column / document)
huge write volume, flexible schema, horizontal scale, eventual is fine.
(c) Semantic search -> Vector store (pgvector to start)
nearest-neighbour over embeddings — neither SQL nor NoSQL does this natively.
The senior move: start with the simplest store that works (often Postgres + pgvector) and only reach for specialised systems when scale or access patterns force it.
Drill 3 caching
A product page makes 5 slow DB queries per request and gets 10,000 reads/min, but the data changes only a few times a day. Where do you put a cache, and how do you invalidate it?
# Cache the assembled product page (or its query results) in Redis key = f"product:{id}" val = redis.get(key) or load_from_db_and_set(key, ttl=3600) # Invalidation — data changes rarely, so: 1. Write-through / explicit bust: on product update, delete the key 2. Plus a TTL as a safety net (e.g. 1h) in case a bust is missed
Read-heavy + rarely-changing is the ideal cache case. Pair explicit invalidation (precise) with a TTL (forgiving) — belt and braces.
Drill 4 sync vs async
For DocChat, decide sync or async for each, and justify: (a) returning a chat answer, (b) ingesting an uploaded PDF, (c) sending a "your document is ready" email.
(a) Chat answer -> SYNC
the user is waiting on this exact response; ~3s is acceptable.
(b) PDF ingestion -> ASYNC (queue + workers)
slow (parse/chunk/embed), depends on rate-limited LLM API.
Return status:"processing" instantly, finish in the background.
(c) Ready email -> ASYNC
nobody is blocking on it; fire it from a worker after ingestion.
Rule of thumb: if a human is staring at the screen waiting for this result, it's sync. If the work is slow, can fail/retry, or nobody's blocked — push it to a queue.
Drill 5 bottleneck hunt
A design has: load balancer → app servers → a single Postgres primary handling all reads and writes, with file uploads written as BLOBs into that same database. Traffic 10x's. What breaks first, and how do you fix it?
Bottleneck: the single Postgres primary — three problems at once.
1. Read load saturates it -> add READ REPLICAS, route reads to them.
2. File BLOBs bloat it -> move files to OBJECT STORAGE (S3),
keep only the s3_key in the DB.
3. Write throughput ceiling -> cache hot reads (Redis); partition/shard
later if writes still exceed one primary.
Storing files in the DB is the smell to spot instantly. The general lesson: a single primary doing everything is the classic first bottleneck — separate concerns (files → S3, reads → replicas, hot data → cache).
Build · Design DocChat for 100k users
Prompt: "Design a SaaS where users upload private documents and ask an AI questions about them. Assume 100k users." Drive the whole conversation.
# 1. REQUIREMENTS Functional: upload doc, ask questions over your own docs, see history. Non-functional: ~100k users; reads light; answer <3s; uploads can be eventually-searchable; never lose a file; LLM cost matters. # 2. ESTIMATES 100k users * 5 q/day = 500k/day -> ~5 avg / ~15 peak QPS (tiny). 100k * 20 docs * 2MB = 4 TB files -> S3. 2M docs * 40 chunks = 80M vectors * 6KB ~ 480 GB -> the real constraint. Cost: 500k LLM answers/day -> cache, batch, fall back. # 3. API POST /documents | GET /documents/:id | POST /chat | GET /chat/history # 4. DATA MODEL (managed Postgres + pgvector) users(id, org_id, ...) documents(id, user_id, org_id, s3_key, status, content_hash) chunks(id, document_id, user_id, org_id, text, embedding vector(1536)) messages(id, user_id, conversation_id, role, content, created_at) # 5. ARCHITECTURE (split fast read path / slow write path) CDN -> Load Balancer -> stateless App Servers App -> Redis (cache + rate limit), Postgres primary (+ read replicas for pgvector search), S3 (files), Queue Queue -> Workers: parse -> chunk -> embed (LLM) -> store vectors Upload: app stores file in S3, writes documents row status="processing", enqueues a job, returns instantly. Workers (idempotent on content_hash) finish; user polls until status="ready". # 6. DEEP-DIVE: vector search scaling Start pgvector + HNSW -> read replicas -> partition by org_id (also gives tenant isolation) -> dedicated vector store only when recall/latency demand. # 7. BOTTLENECKS Ingestion throughput (LLM rate limit) -> more workers + batching + queue. Vector search at 100s of millions -> partition, then dedicated store. # 8. TRADE-OFFS (say them!) Eventual consistency on upload: not instantly searchable, but fast/resilient. pgvector now vs dedicated later: a future migration, but ships faster today. Multi-tenancy: every query scoped by user_id/org_id — isolation is mandatory. Reliability: timeouts + retries(backoff/jitter) + circuit breaker, model fallback. Observability: log QPS, p95 latency, queue depth, LLM spend, worker failures.
The grade is in the shape: did you clarify before designing, let the math pick the hard problems, separate read/write paths with a queue, make workers idempotent, and name your trade-offs unprompted? If yes, you ran a senior interview.
Click a card to flip it. Say the answer out loud before you flip — that's the rep that survives interview nerves.
Tick each only if you can do it without looking: