Organize the answer around ownership, limits, failure, and recovery. Definitions become interview-ready when they survive a concrete production scenario.
Question set
29 detailed answers
01Step 1. Clarify the requirements
junior
Never start designing right away. First clarify exactly what you're building. Split the requirements into two groups.
Functional requirements — what the system does (features):
- What are the main scenarios? (e.g.: "shorten a link" and "follow a short link").
- Who are the users? How many? Geographic distribution?
- What's NOT in scope? (analytics, authentication, billing — often can be dropped, as long as you say so explicitly).
Non-functional requirements (NFRs) — what properties it has:
- Scale: how many users / requests / data.
- Availability: is "always respond" more important, or "respond correctly"? (CAP).
- Latency: hard requirements (p99 < 100 ms) or not.
- Consistency: is eventual consistency acceptable, or do you need strong consistency.
- Durability: can data be lost (logs vs payments).
- Read/Write ratio: is reading or writing dominant.
💡 Ask questions out loud and record the answers — that alone is half the evaluation. Example: "Do we need click analytics? If so, that changes the data model. I'll assume only a basic counter is in scope."
02Step 2. Estimate the scale (back-of-the-envelope)
junior
Rough estimates that will shape the architecture. Calculate out loud and round aggressively.
What to estimate:
- QPS (queries per second): average and peak (peak ≈ 2-3× the average).
- Data volume: size of one record × number of records × retention horizon.
- Read/write ratio (read:write) — determines whether you need read replicas and a cache.
- Bandwidth: QPS × response size.
- Storage: growth per year.
Handy numbers for mental math:
| Quantity | Value |
|---|---|
| Seconds in a day | ~86,400 ≈ 10⁵ |
| Month | ~2.5M seconds |
| 1M requests/day | ≈ 12 QPS on average |
| 1B requests/day | ≈ 12,000 QPS |
Example calculation (URL shortener): 100M new links/month → 100M / 2.5M sec ≈ 40 writes/sec. Read:write = 100:1 → 4000 reads/sec. Record size ~500 bytes → 100M × 500B = 50 GB/month → 600 GB/year → ~6 TB over 10 years.
💡 State the conclusion from the numbers right away: "40 writes/sec is easy for a single DB. 4000 reads/sec — we'll add a cache and read replicas."
03Step 3. Define the API
junior
Describe the service contract — this pins down the functionality and helps the interviewer understand the model. REST/gRPC, the main endpoints:
POST /api/v1/urls {long_url, custom_alias?, ttl?} -> {short_url}
GET /{short_key} -> 301/302 redirect
Talk through: methods, parameters, idempotency (is POST idempotent?), pagination for lists (cursor-based, not offset on large data sets), authorization (api_key / token).
The contract must cover more than the happy path: error codes, size limits, timeouts, and retry behavior. For resource creation the client can send an Idempotency-Key; the server stores the key with the result and returns the same response on a retry. This makes the API a testable system boundary rather than a list of accidental URLs.
request → validate/auth → domain operation → stable response
│ │
4xx contract retry-safe effect
04Step 4. Data model and database choice
junior
- Describe the key entities and relationships (tables / collections).
- Choose the database type and justify it:
- SQL (Postgres, MySQL): complex relationships, transactions, strong consistency, analytical queries, JOINs. Take it by default unless there's a reason not to.
- NoSQL key-value / wide-column (DynamoDB, Cassandra): huge write scale, a simple key-based access pattern, horizontal sharding out of the box, eventual consistency.
- Document (MongoDB): flexible schema, nested documents.
- In-memory (Redis): cache, counters, rate limiting, queues, leaderboards.
- Think about the shard key right away (what you partition by).
05Step 5. High-level architecture
junior
Describe in words (or with a diagram) the components and the request flow:
[Client] -> [DNS] -> [Load Balancer] -> [API / App Servers (stateless)]
|-> [Cache (Redis)]
|-> [Database (+ read replicas)]
|-> [Message Queue] -> [Workers]
[CDN] -> [Blob Storage (S3)]
Trace a typical request along this path from the client all the way to the DB and back.
Do not draw every component you know. Start with the minimum path that satisfies the requirements, then add a cache, queue, or replica only when the estimates justify it. For every edge state the protocol, sync/async behavior, timeout, and source of truth; for every stateful component state ownership, replication, and recovery.
Walk both a normal request and a failure. For example, a cache miss reads the database and fills the cache; if the database is unavailable, the request stops at a bounded timeout, a circuit breaker limits the cascade, and metrics expose the degraded path. A component diagram only becomes an architecture when its flows and failures are explained.
06Step 6. Drilling into the key components
middle
Go deep on the 1-2 most interesting parts (the ones that make the problem distinctive): key generation, the feed ranking algorithm, message delivery, the caching scheme. This is where the main depth lives.
For the selected part, define the data and invariants, walk the algorithm on one concrete request, estimate complexity, and analyze at least one failure. For ticket reservation, for example, the invariant is sold + held ≤ capacity; show the atomic conditional update, hold TTL, and idempotent payment retry rather than merely drawing service boxes.
requirement → invariant → algorithm → state → failure → recovery
Finish with a measurable trade-off: what the design improves, what it costs, and which workload change would force you to revisit it.
07Step 7. Bottlenecks, scaling, fault tolerance, trade-offs
middle
- Where is the bottleneck? (usually the DB on reads/writes, or a single component).
- How to scale each layer (horizontal scaling is preferred).
- Where is the single point of failure? How to make it redundant.
- Which trade-offs you accepted and why (consistency vs availability, cost vs latency).
💡 A good answer always ends with something like: "Here I chose X, sacrificing Y, because for this scenario Z matters more. The alternative is W, which is better under a different load profile."
08Load Balancer and Reverse Proxy
junior
- LB distributes traffic across instances (round-robin, least-connections, consistent hashing). It enables horizontal scaling and fault tolerance (removing dead nodes via health checks).
- L4 (TCP) is faster; L7 (HTTP) can route by URL/headers and do TLS termination.
- Reverse proxy (Nginx, Envoy): TLS termination, compression, caching, backend protection, a single entry point.
- 💡 Keep applications stateless — then the LB can send a request to any instance. Sessions go in Redis, not in process memory.
09Caching
junior
- Where: on the client, the CDN, the application layer (Redis/Memcached), and in the DB.
- Patterns:
- Cache-aside (lazy): the application reads the cache, falls back to the DB on a miss, then populates the cache. The most common one.
- Write-through: write to cache and DB synchronously (consistent, but slower writes).
- Write-back: write to the cache, and to the DB asynchronously (fast, but risk of data loss).
- Invalidation (one of the two hardest problems in CS): TTL, explicit deletion on write, key versioning.
- Problems: cache stampede (many misses at once → locking/early recompute), hot keys, cache/DB consistency.
- 📎 For details on Redis, data structures, eviction, and patterns — see the Redis file (
../<redis>.md).
10CDN
junior
- Caches static assets (images, video, JS/CSS) closer to the user (edge). Reduces latency and load on the origin.
- Use it for any heavy static / rarely changing content and for a geographically distributed audience.
The cache key consists of the URL plus selected request headers. Content-hashed filenames support a long Cache-Control: immutable; mutable HTML needs a short TTL or an explicit purge. Dynamic responses are cacheable only when they are public and the key includes every variant—never let personalized cookies collapse into one shared response.
client ─► nearest edge ──HIT──► response
│
MISS ─► origin ─► cache at edge
⚠️ A CDN does not make the first cache miss faster. Protect the origin from stampedes and track hit ratio separately by content type.
11Databases: replication, sharding, partitioning
middle
- Replication (read replicas): copies of the DB for scaling reads and for fault tolerance. Writes go to the primary, reads to the replicas. Trade-off: replication lag → eventual consistency on reads.
- Sharding: horizontally splitting data across multiple DBs by a key. Scales writes and volume. Strategies: by key hash, by range, by geo. Downsides: complex JOINs/transactions across shards, rebalancing, hot shards.
- Partitioning — the same thing within a single DB (by date range, by list). Simplifies working with large tables.
- Choosing the shard key is critical: it must distribute evenly and match the access pattern.
12Queues and asynchronous processing
middle
- A Message Queue (Kafka, RabbitMQ, SQS) decouples producer and consumer. Use it for: heavy tasks outside the user's request (sending email, video processing, feed fan-out), smoothing out load spikes (a buffer), and retries on failures.
- Delivery guarantees: at-most-once / at-least-once / exactly-once. Most often at-least-once + idempotent consumers.
- Publish through a transactional outbox; make consumers deduplicate a stable
message_idin the same transaction as the business effect. Send transient failures to a retry topic with backoff and exhausted messages to a DLQ for investigation. - A queue moves load through time; it does not remove it. Monitor lag, oldest-message age, and throughput, and define backpressure plus an explicit drop/degradation policy before the buffer fills.
13Indexes and denormalization
middle
- Indexes speed up reads at the cost of slower writes and extra space. Create them for your actual queries (WHERE/JOIN/ORDER BY).
- Denormalization: duplicating data to avoid JOINs on reads. Use it in read-heavy systems (feed, profiles). Trade-off: harder to keep consistent on writes.
Start from an access pattern: an index on (tenant_id, created_at DESC) serves a particular filter and ordering, while indexing everything increases write amplification and maintenance time. For each denormalized copy name the source of truth and update mechanism—same transaction, outbox/CDC, or periodic rebuild—and define the acceptable stale window plus reconciliation after a missed event.
write → source of truth → outbox/CDC → read model
↘ retry + reconciliation
14CAP theorem and consistency
middle
- During a network partition (P, unavoidable in distributed systems) you choose between C (consistency) and A (availability):
- CP: during a partition you sacrifice availability for correctness (banks, inventory). Example: HBase, etcd.
- AP: keep responding, allowing divergence (feed, likes). Example: Cassandra, DynamoDB.
- In practice PACELC matters more: even without a partition (Else) there's a choice of Latency vs Consistency.
CAP describes behavior during a partition; it does not label a system permanently as “CA.” A CP operation rejects some requests to preserve one valid history, while an AP operation accepts them and resolves conflicts later. Decide per invariant: money transfer needs correctness, whereas a like counter can temporarily diverge.
partition? ─ yes ─► consistency or availability
└ no ─► PACELC: latency or consistency
⚠️ Replication alone does not provide strong consistency. Leadership, quorums, read rules, and failover behavior determine the actual guarantee.
15Consistency: strong vs eventual, quorums
middle
- Strong consistency: any read sees the latest write. More expensive, higher latency. Needed for money, balances, uniqueness.
- Eventual consistency: replicas converge over time. Cheap and available. Fine for likes, view counters, the feed.
- Quorums: with N replicas, require acknowledgment from W on writes and R on reads. If W + R > N — fresh reads are guaranteed (for example N=3, W=2, R=2). Tuning W/R balances consistency, latency, and availability.
16Idempotency and dedup
middle
- Idempotency: repeating an operation yields the same result (a retried payment POST doesn't charge twice).
- Implementation: an idempotency key from the client → the server stores the result by key and, on a retry, returns the saved result without performing the operation again. Store it in Redis/DB with a TTL.
- Message dedup in queues — by message_id.
The key check and business effect must be atomic. A reliable design stores an idempotency_key row with a UNIQUE constraint in the same database transaction; checking Redis separately before the write leaves a race window. Persist the status and serialized response, reject the same key with a different payload, and retain it longer than the maximum client retry window.
same key + same payload → same stored result
same key + other payload → 409 conflict
17Rate Limiting
middle
- Rate limiting protects capacity and constrains abuse. Fixed window is simple but permits a double burst at a boundary; sliding window/log is more accurate but stores more state; token bucket permits a controlled burst and replenishes at a steady rate.
- Key the limit by user, API key, tenant, or IP and return
429withRetry-After. Perform it before expensive work but after identifying the caller well enough. - In a distributed system, keep state in Redis and update it atomically with Lua. Decide whether a Redis outage fails open for a noncritical API or fails closed for an expensive or sensitive operation.
request → identify key → consume token? ─ yes → handler
└ no → 429 + Retry-After
18Microservices vs monolith
middle
- Monolith: easier to develop, deploy, and debug; a single transaction. Take it by default for a new product.
- Microservices: independent scaling and deployment per team, fault isolation. The cost: network calls, distributed transactions (saga), complex observability, eventual consistency between services.
- 💡 In an interview: "I'd start with a modular monolith and split out into services whatever needs to scale independently or is owned by a separate team."
19API Gateway
middle
- A single entry point: routing, authentication, rate limiting, response aggregation, versioning, TLS. Offloads cross-cutting concerns from the services. The risk is becoming a SPOF/bottleneck, so it's scaled and made redundant.
Do not turn the gateway into a new business-logic monolith. It may validate a token and enforce common policy, but resource-level authorization belongs to the service that owns the data. Run multiple stateless instances behind a load balancer, use short downstream timeouts, cap request bodies, and observe latency/error rate per route. Response aggregation also needs partial-result and fallback behavior when one downstream fails.
client → gateway ┬→ users
├→ orders
└→ catalog
20Blob Storage (S3)
junior
- Object storage for files, images, video, backups. Cheap, durable (internal replication), infinitely scalable.
- Pattern: metadata in the DB, the file itself in S3, served via CDN, uploaded via a pre-signed URL (the client writes to S3 directly, bypassing the backend).
An object is addressed by key and normally replaced as a whole; object storage is not a POSIX filesystem and is unsuitable for frequent in-place updates. Versioning and lifecycle rules can archive or delete old objects. A pre-signed URL should have a short lifetime, a restricted method, and an unpredictable key; after upload, verify size and file signature and run asynchronous malware scanning before making it public.
client ──metadata──► API ──signed URL──► client
client ─────────────upload──────────────► object storage
21Search (Elasticsearch)
middle
- Full-text search, facets, aggregations (an inverted index). Not the primary store — data comes from the main DB via CDC/a queue. Eventual consistency is acceptable.
An inverted index maps a term to matching documents; the analyzer performs tokenization, normalization, and stemming. Design mappings deliberately: text fields support full-text relevance, while keyword fields support exact filters and aggregations. A common write path is DB → outbox/CDC → indexer → search index; events must be idempotent and versioned, and periodic reconciliation repairs missed updates.
⚠️ Do not make Elasticsearch the sole source of truth for payments or inventory. Refresh and replication create a stale window, and rebuilding an index is a normal operational event.
22Geo-distribution and latency
middle
- Place services and data closer to users (multi-region), use a CDN and GeoDNS / Anycast.
- The speed of light sets a lower bound on latency: an intercontinental round-trip is ~100-200 ms. Within a datacenter — < 1 ms.
- Trade-off: multi-region complicates consistency (geo-distributed writes → conflicts).
Separate reads from writes first: static content and read replicas can move closer to users, while writes stay in one home region to preserve simple ordering and transactions. Active-active writes need key ownership or conflict resolution; last-write-wins can lose a valid update because wall clocks disagree. For money or unique inventory, route a key to one leader and accept the cross-region delay.
user → nearest edge/read replica
write → home region leader → async replicas
23SPOF and fault tolerance
middle
- A Single Point of Failure is a component whose failure brings down the system. It's eliminated by redundancy: multiple instances behind an LB, DB replication with failover, multi-AZ.
- Resilience patterns: health checks, retries with backoff, circuit breaker, graceful degradation (serve the cache/a partial response), timeouts, bulkheads.
Check shared dependencies, not only instance count: two application replicas in one AZ, one DNS service, a shared connection pool, or manual failover can still be a SPOF. Retry only safe/idempotent operations with exponential backoff, jitter, and an overall deadline. Circuit breakers constrain a sick dependency, bulkheads keep it from consuming every worker, and graceful degradation preserves the critical path.
⚠️ A replica without a regularly tested failover is a copy, not fault tolerance. Run game days and measure actual RTO and RPO.
24Scale metrics (orders of magnitude)
concept
Rough reference points for estimates (order of magnitude, not exact numbers):
| Component | Approximate capacity |
|---|---|
| 1 app server | ~1,000–10,000 QPS (depends on the logic) |
| 1 DB (writes) | ~1,000–10,000 writes/sec |
| 1 read replica | tens of thousands of simple reads/sec |
| Redis (single instance) | ~100,000+ ops/sec |
| A relational DB row | hundreds of bytes – a few KB |
| Network round-trip in a datacenter | < 1 ms |
| SSD read | ~100–200 µs |
| Inter-region RTT | ~50–150 ms |
253.1 URL Shortener (TinyURL)
1. Requirements. Functional: shorten a long URL into a short one; redirect from the short key to the original; (optional) custom alias, TTL. Out of scope: click analytics, accounts. NFR: very read-heavy (far more redirects than creations); low redirect latency; high availability; short links are unique and non-guessable; eventual consistency is acceptable for redirects.
2. Scale. 100M new links/month → ~40 writes/sec. Read:write ≈ 100:1 → ~4000 reads/sec, peak ~10k. Storage: 100M × ~500 B = 50 GB/month → ~6 TB over 10 years. Key length: a base62 alphabet (a-zA-Z0-9). 62⁷ ≈ 3.5 trillion — 7 characters are enough for a long time.
3. API.
POST /api/v1/shorten {long_url, custom_alias?, ttl?} -> {short_url}
GET /{key} -> 301/302 -> long_url
301 (permanent) is cached by the browser forever → less load, but you lose analytics; 302 (temporary) → every click hits the server. Discuss the choice.
4. Data.
Table urls(key PK, long_url, created_at, expires_at, creator_id). The access pattern is a point lookup by key → maps perfectly onto a key-value NoSQL store (DynamoDB/Cassandra) for scale, or Postgres with an index on key to start. Sharding by key (hash) is even.
5. Key generation (the central part 🟡):
- Option A — hash (MD5/SHA of the URL, take the first 7 base62 characters): simple, but collisions → check and, on a collision, add salt/re-hash. The same URL produces the same key (can be seen as a pro or a con).
- Option B — counter + base62: a global auto-increment → encode it in base62. Guaranteed collision-free, short keys. Downside: a single counter is a bottleneck/SPOF. The fix is ID ranges (a ticket/range server hands out blocks of 1000 to each app server) or a distributed generator (Snowflake/ZooKeeper). The keys are predictable → you can shuffle them.
- 💡 I usually propose B with range handout — it scales and is collision-free.
6. Architecture.
Client -> LB -> App Servers -> [Redis cache] -> [KV DB (sharded)]
-> [ID/range service] (for writes)
Redirect: the app checks Redis (cache-aside, TTL), on a miss hits the DB, populates the cache, and returns a 302.
7. Bottlenecks.
- Reads are the main volume. Solved with a cache (hot links) + read replicas/wide sharding. The hit rate is high because the click distribution follows a power law (popular links).
- Writes — handing out ID ranges removes contention.
- SPOF — make the LB and ID service redundant, replicate the DB.
- Cleaning up expired links — a background job / TTL in the DB.
263.2 News Feed
1. Requirements. Functional: a user sees a feed of posts from the people they follow, sorted (by time/relevance); publishing a post; follows. NFR: read-heavy; low feed-load latency (this is the hot path); eventual consistency is fine (a couple of seconds' delay before a post appears is acceptable); high availability.
2. Scale. Say 300M DAU, each opening the feed ~10 times/day → 3B reads/day ≈ 35k QPS, peak ~70k. Posts: 100M/day. Average number of follows ~ hundreds; for celebrities — tens of millions of followers (an important special case).
3. API.
GET /v1/feed?cursor=...&limit=20 -> [posts]
POST /v1/posts {content, media_ids} -> {post_id}
POST /v1/follow {target_user_id}
Pagination — cursor-based (by post_id/timestamp), not offset.
4. Data.
posts(post_id, author_id, content, media, created_at)follows(follower_id, followee_id)feed_cache(precomputed): a per-user list of post_ids (Redis list / sorted set). SQL for the follow graph and posts; Redis for the materialized feeds.
5. Fan-out — the key decision 🔴:
- Fan-out on write (push): when a post is published, immediately write its id into the feeds (Redis) of all followers. Reading the feed → a fast GET of a ready list. Pro: lightning-fast reads. Con: expensive writes for popular authors (the "fan-out problem": a celebrity's post with 50M followers = 50M writes).
- Fan-out on read (pull): assemble the feed at request time — take posts from everyone you follow and merge them. Pro: cheap writes. Con: expensive and slow reads, especially with many follows.
- Hybrid (the right answer): regular users — push; celebrities (above a follower-count threshold) — pull. On read, merge the precomputed feed with the "live" posts of the celebrities you follow. 💡 This is how real social networks do it.
6. Architecture.
Post -> API -> posts DB -> [Queue] -> Fan-out workers -> Redis feed lists
Read feed -> API -> Redis (ready feed) + pull celebrity posts -> merge -> rank
Fan-out is asynchronous via a queue so it doesn't block publishing.
7. Ranking and bottlenecks.
- Ranking: chronological is simple; ML ranking (relevance) is a separate service with scores; mentioning it is enough in an interview.
- Bottlenecks: celebrity fan-out (→ hybrid), Redis memory (store only the N most recent post_ids per user, the rest is a pull from the DB), hot keys.
- Fault tolerance: the feed cache can be rebuilt from the DB; Redis replicas.
273.3 Chat / messenger (WhatsApp-like)
1. Requirements. Functional: 1-on-1 messages, group chats, delivery statuses (sent/delivered/read), presence (online/last seen), history. NFR: low delivery latency; high availability; message durability (don't lose them); message ordering within a chat; scale of connections.
2. Scale. 500M DAU, 40 messages/day → 20B messages/day ≈ 230k msg/sec. Concurrent connections — hundreds of millions. Storage: 20B × ~200 B ≈ 4 TB/day.
3. API / protocol. Not classic REST for delivery — you need bidirectional communication: WebSocket (or MQTT) for live, plus HTTP for history and registration.
WS: connect; send {chat_id, msg}; receive {msg}; ack {msg_id}
GET /v1/chats/{chat_id}/messages?cursor=...
4. Data.
messages(chat_id, msg_id, sender_id, content, created_at, status)— partitioned/sharded bychat_id, sorted by time. Wide-column (Cassandra) is ideal: huge write volume, access by chat key, time-series. msg_id — Snowflake (monotonic → ordering).chats,chat_members,user_presence.
5. Delivery — the key part 🔴:
- Users hold a persistent WebSocket connection to gateway servers (stateful). You need a registry: which user is on which gateway → store it in Redis (
user_id -> gateway_id). - Sending: A writes to its gateway → the message service saves it to the DB (durability) → finds recipient B's gateway → pushes over B's WS. B sends an ack → status becomes delivered.
- B offline: the message sits in the DB/queue; when B connects, it requests undelivered messages (since the last seen msg_id). A push notification (APNs/FCM) is an option.
- Groups: fan out the message to members (for large groups — like feed fan-out).
6. Presence.
A heartbeat over WS every N seconds → update last_seen in Redis with a TTL. No heartbeat → offline. Don't push presence to everyone — only to active observers.
7. Bottlenecks.
- Millions of persistent connections: many gateway servers, a connection LB (L4, sticky). Each server holds ~tens to hundreds of thousands of connections.
- Routing between gateways → a Redis registry + pub/sub.
- Ordering and idempotency: a client-generated msg_id for dedup on retries.
- Durability: write to the DB before acknowledging the sender.
283.4 Rate Limiter
1. Requirements. Limit the number of requests per client (user/IP/API key) over a period: for example 100 req/min. NFR: low overhead and latency; accuracy; works in a distributed environment (many instances); fail-open or fail-closed when the storage fails (discuss).
2. Scale. It runs on every request → it must be very fast (< 1 ms). At 100k QPS — 100k checks/sec.
3. API.
Usually middleware / part of the API Gateway. allow(key) -> bool. On rejection → HTTP 429 + headers X-RateLimit-Remaining, Retry-After.
4. Algorithms (the central part 🟡):
- Fixed window counter: a counter per window (a minute). Simple, but a burst at the window boundary (up to 2× the limit).
- Sliding window log: store the timestamp of every request, count over the last 60 sec. Accurate, but expensive in memory.
- Sliding window counter: interpolation between the current and previous window. A good balance of accuracy and memory. A common choice.
- Token bucket: the bucket fills with N tokens/sec, a request takes a token; empty → rejection. Allows bursts up to the bucket size. Very popular (flexible).
- Leaky bucket: requests go into a queue, processed at a constant rate. Smooths out traffic.
5. Distributed implementation 🔴:
- State is shared across all instances → Redis (fast, atomic). Counter/tokens by key
rl:{user}:{window}with a TTL. - Atomicity — a Lua script (read-check-write in one operation) or
INCR+EXPIRE. Without atomicity — races and overshooting the limit. - Latency trade-off: a network call to Redis on every request. Optimization — a local cache + periodic synchronization (sacrificing a bit of accuracy).
- When Redis is unavailable: fail-open (let requests through so as not to take down the service) vs fail-closed (block) — decide by requirements.
6/7. Bottlenecks. Redis as a hot spot → shard by client key; replicas. A hot key from one large client — a dedicated shard / local limiting.
293.5 File / video storage and serving
1. Requirements. Functional: upload a file/video, store it, download/stream it, (for video) transcode into different qualities. NFR: durability (don't lose files); high throughput; low serving latency globally; large volume.
2. Scale. Say 1M uploads/day, average file 5 MB → 5 TB/day of new content; reads are tens of times higher → terabits of bandwidth → a CDN is mandatory.
3. API.
POST /v1/files/initiate {filename, size, content_type} -> {upload_id, presigned_url}
PUT <presigned_url> (client uploads directly to S3, multipart for large files)
POST /v1/files/complete {upload_id} -> {file_id, url}
GET /v1/files/{file_id} -> 302 -> CDN URL
4. Data.
- Metadata in the DB:
files(file_id, owner, name, size, s3_key, status, created_at)— a relational DB is fine. - The content itself — blob storage (S3), not the DB. Large files — multipart upload (chunks, in parallel, resumable).
5. Architecture (the central part 🟡):
Client --presigned PUT--> S3 (origin)
<--metadata-- App Server -- DB
S3 --event--> Queue --> Transcoding Workers --> S3 (variants 240p/720p/1080p)
Client --GET--> CDN (edge) --miss--> S3
- Pre-signed URL: the backend issues a temporary URL, the client writes straight to S3, bypassing the app servers (we don't push terabytes through the backend).
- Video transcoding: an upload triggers an event → a queue → a pool of workers slices it into segments and encodes it into several qualities/bitrates (for adaptive streaming, HLS/DASH). Asynchronous, idempotent.
- CDN serves both static assets and video segments — the main read layer.
6/7. Bottlenecks and resilience.
- Read bandwidth → the CDN takes the bulk of the load off the origin.
- Durability → S3 replicates objects across AZs; for critical data — cross-region replication.
- Transcoding is CPU-heavy → scale workers by queue length (autoscaling); retries on failures; task status in the DB.
- Large uploads → multipart + resume + checksum verification.
- Deduplication by content hash (store identical files once).
Source notes
References and review policy
RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.