interviewDeck

Your one-stop platform to prepare, practice and ace your interviews.

Loading your questions…

All Questions

Filters & tools

System Design Interview Questions and Answers

27 hand-picked System Design interview questions with detailed answers. Open the interactive version above to search, filter by difficulty, run code, bookmark questions and track your progress.

What is a system design interview, and what are interviewers evaluating?

A system design interview asks you to design a real-world system (URL shortener, chat, feed, etc.) on a whiteboard or shared doc. Interviewers evaluate:

  • Structured thinking — clarify requirements before jumping to boxes.
  • Trade-off reasoning — why this DB, why cache here, what breaks at scale.
  • Scalability & reliability — bottlenecks, failure modes, redundancy.
  • Communication — narrate assumptions, ask questions, iterate.

At 2–4 YOE, depth on every component isn't expected — a clear framework and sensible choices matter more than memorized architectures.

Functional vs non-functional requirements — how do you gather them?

Functional requirements describe what the system does: features, user flows, APIs, data stored.

Non-functional requirements (NFRs) describe how well it must perform: latency, throughput, availability, consistency, security, cost.

Always clarify both before designing. Example for a URL shortener:

  • Functional: shorten URL, redirect, optional custom alias, analytics.
  • Non-functional: 100M URLs, 10K redirects/sec, 99.9% uptime, redirect < 100ms p99.

Vertical vs horizontal scaling — when do you use each?

Vertical scaling (scale up) — add more CPU/RAM/disk to one machine. Simple, but hits hardware limits and creates a single point of failure.

Horizontal scaling (scale out) — add more machines behind a load balancer. Better fault tolerance and theoretically unlimited scale, but requires stateless app servers, distributed data, and operational complexity.

Typical path: optimize code → vertical scale → horizontal scale + caching + DB scaling.

What is load balancing and what algorithms do you know?

A load balancer distributes incoming traffic across multiple servers to improve throughput and availability. It also enables health checks and zero-downtime deploys.

Common algorithms:

  • Round robin — simple, even distribution.
  • Least connections — send to server with fewest active connections.
  • Consistent hashing — sticky routing by key (user ID) — useful for caches.
  • IP hash — same client → same server (session stickiness).

Layers: L4 (TCP, fast) vs L7 (HTTP-aware, path-based routing).

What caching strategies do you use in system design?

Caching stores frequently accessed data closer to the consumer to reduce latency and DB load.

  • Cache-aside — app checks cache, on miss reads DB and populates cache. Most common.
  • Read-through — cache library fetches from DB on miss.
  • Write-through — write to cache and DB together.
  • Write-behind — write to cache, async flush to DB (fast, riskier).

Always plan for cache invalidation, TTLs, and cache stampede (many misses at once).

What is a CDN and when should you use one?

A CDN (Content Delivery Network) caches static content (images, JS, CSS, videos) at edge servers geographically close to users. Benefits:

  • Lower latency — content served from nearest PoP.
  • Reduced origin load — 80–95% of static traffic off your servers.
  • DDoS absorption and TLS termination at the edge.

Use for static assets and cacheable API responses. Don't put personalized or highly dynamic data on CDN without careful cache keys.

How do you scale a database — read replicas and sharding?

Read replicas — copy primary DB data to replicas; route reads to replicas, writes to primary. Good for read-heavy workloads. Watch replication lag for stale reads.

Sharding (partitioning) — split data across multiple DBs by a shard key (user_id, tenant_id). Scales writes and storage, but cross-shard queries and rebalancing are hard.

Typical progression: optimize queries → indexes → read replicas → caching → sharding → specialized stores.

Explain the CAP theorem and what it means in practice.

CAP theorem: in a network partition, a distributed system can guarantee at most two of:

  • C — Consistency — every read returns the latest write.
  • A — Availability — every request gets a response (not error/timeout).
  • P — Partition tolerance — system works despite network failures between nodes.

In practice, partitions happen, so you choose between CP (consistent but may reject requests) and AP (available but may return stale data). Most web systems are AP with tunable consistency.

Strong consistency vs eventual consistency — when to use each?

Strong consistency — after a write, all reads see the new value immediately. Requires coordination (single leader, quorum, locks). Higher latency, lower availability on failures.

Eventual consistency — replicas converge over time; reads may be stale briefly. Higher availability and performance.

Variants: read-your-writes, monotonic reads, causal consistency — pragmatic middle ground for user-facing apps.

REST APIs vs event-driven design — when do you use each?

REST (synchronous) — client calls API, waits for response. Simple, good for request/response flows (CRUD, queries). Couples caller to callee availability.

Events (asynchronous) — producer publishes event; consumers process independently via message broker. Decouples services, handles spikes, enables replay. Harder to debug; eventual consistency.

Use REST for user-facing queries and commands needing immediate feedback. Use events for side effects (send email, update search index, analytics).

What role do message queues play in system design?

Message queues (Kafka, RabbitMQ, SQS) enable async communication between services:

  • Decoupling — producer doesn't need consumer online.
  • Buffering — absorb traffic spikes without overwhelming downstream.
  • Reliability — retry failed processing; dead-letter queues for poison messages.
  • Scalability — add consumers to parallelize work.

Design for at-least-once delivery + idempotent consumers unless you truly need exactly-once.

How does rate limiting work and where do you apply it?

Rate limiting caps requests per user/IP/API key over a time window to prevent abuse, DDoS, and cost overruns.

Algorithms:

  • Token bucket — tokens refill at fixed rate; burst allowed up to bucket size.
  • Fixed window — N requests per minute; simple but boundary spikes.
  • Sliding window — smoother; more accurate, slightly more complex.

Implement at API gateway or edge (Cloudflare, Kong). Store counters in Redis for distributed rate limiting. Return 429 Too Many Requests with Retry-After header.

Design a URL shortener (like bit.ly).

Functional: shorten long URL → short code; redirect short → long; optional custom alias, expiry, click analytics.

High-level: API servers → cache (Redis) → DB (SQL or NoSQL). Load balancer in front. CDN optional for redirect edge caching.

ID generation: base62 encode auto-increment ID, or hash (MD5 truncated — collision risk). Store mapping short_code → long_url.

Redirect flow: GET /{code} → cache lookup → 301/302 redirect. Read-heavy — cache aggressively.

Design a notification system (push, email, SMS).

Functional: send notifications via push/email/SMS; support templates, user preferences, scheduling, delivery status.

Architecture: API receives send request → validate → publish to message queue → channel-specific workers (email worker, push worker, SMS worker) → third-party providers (SendGrid, FCM, Twilio).

Store notification history and user preferences. Use priority queues for OTP vs marketing. Retry with backoff; dead-letter queue for failures.

Design a chat/messaging system (1:1 and group).

Functional: 1:1 and group chat, message history, online status, read receipts, media attachments.

Real-time: WebSockets (or long polling fallback) between client and chat servers. Use a gateway layer for persistent connections.

Storage: messages in NoSQL/Cassandra (write-heavy, time-ordered) or SQL sharded by conversation_id. Media in object storage (S3).

Delivery: publish message to queue → fan-out to recipient's connected server via pub/sub (Redis). Offline users fetch history on reconnect.

Design an e-commerce shopping cart.

Functional: add/remove items, update quantity, view cart, merge guest → logged-in cart, checkout handoff.

Storage: cart data in Redis (fast, TTL for abandoned carts) keyed by user_id or session_id. Persist to DB on login or checkout.

Inventory: don't hard-reserve on add-to-cart — show availability from inventory service. Reserve stock at checkout with a short TTL lock.

Consistency: cart is per-user/session; eventual consistency OK. Price snapshot at checkout, not on every cart view.

Microservices vs monolith — tradeoffs and when to choose each.

Monolith — single deployable unit. Pros: simple dev, debug, deploy, transactions. Cons: scales as one block, tight coupling over time.

Microservices — independently deployable services. Pros: team autonomy, scale per service, tech diversity. Cons: distributed complexity, network latency, eventual consistency, operational overhead.

At 2–4 YOE startups: start monolith, split when you have clear bounded contexts and team pain — not preemptively.

What is observability — logs, metrics, and traces?

Observability is the ability to understand system health and debug issues from external outputs.

  • Logs — discrete events with context (errors, request details). Centralize with ELK/Datadog. Structured JSON logs are searchable.
  • Metrics — aggregated numbers over time (QPS, latency p99, error rate, CPU). Alert on thresholds. Prometheus + Grafana.
  • Traces — follow one request across services (trace_id spans). Essential for microservices. Jaeger, Zipkin, OpenTelemetry.

Design for observability from day one — you can't debug production without it.

Back-of-envelope estimation basics for system design.

Round aggressively. Useful numbers:

  • 1 day ≈ 86,400 sec ≈ 100K sec
  • 1M requests/day ≈ 12/sec average (×10 for peak ≈ 120/sec)
  • 1 char ≈ 1 byte; 1 KB text ≈ 1K chars
  • 1M users × 1 KB profile ≈ 1 GB
  • SSD read ≈ 1ms; network round-trip ≈ 1–100ms

Template: users → actions/day → QPS → storage per record × records → bandwidth.

What are single points of failure and how do you eliminate them?

A single point of failure (SPOF) is any component whose failure takes down the whole system.

Common SPOFs: single app server, single DB, single load balancer, single region.

Mitigations:

  • Redundancy — multiple instances behind load balancer.
  • Replication — DB primary + replicas with automatic failover.
  • Multi-AZ / multi-region — survive datacenter outage.
  • Health checks — LB removes unhealthy nodes.

What is idempotency and why does it matter in distributed systems?

An operation is idempotent if performing it multiple times has the same effect as once.

Critical because networks fail and clients retry. Without idempotency:

  • Retrying POST /pay → double charge.
  • Retrying message queue consumer → duplicate email.

Solutions: idempotency keys (client sends unique key, server dedupes), natural idempotency (PUT, DELETE), or store processed IDs and skip duplicates.

SQL vs NoSQL — how do you choose a database?

SQL (PostgreSQL, MySQL) — structured schema, ACID transactions, joins, strong consistency. Best for relational data: orders, accounts, inventory with constraints.

NoSQL — flexible schema, horizontal scale, eventual consistency. Types:

  • Document (MongoDB) — nested JSON, catalogs.
  • Key-value (Redis, DynamoDB) — caching, sessions.
  • Wide-column (Cassandra) — time-series, high writes.
  • Graph (Neo4j) — social connections, recommendations.

Default to SQL unless you have a clear reason for NoSQL (massive write scale, flexible schema, geo-distribution).

DRY, KISS, YAGNI, separation of concerns — what do they mean in practice?

  • DRY — one authoritative place per piece of knowledge. The nuance: duplicated code that represents different concepts should NOT be merged — wrong abstraction is worse than duplication.
  • KISS — the simplest design that meets today's requirement; cleverness is a maintenance tax.
  • YAGNI — don't build for imagined future needs (config flags nobody sets, generic engines with one use).
  • Separation of concerns — each module owns one aspect (controller/service/repository; smart/dumb components) so changes stay local.

WebSockets vs SSE vs long polling — and why choose SSE for streaming AI responses?

  • Long polling — client asks, server holds the request until data arrives, repeat. Works everywhere; high overhead per message.
  • SSE (Server-Sent Events) — one long-lived HTTP response streaming text/event-stream. Server→client only; auto-reconnect built into EventSource; plain HTTP (proxies, LBs, HTTP/2 friendly).
  • WebSockets — full-duplex TCP after an HTTP upgrade; both directions, binary support; costs a stateful protocol, trickier LB/proxy story, manual reconnect/heartbeats.

LLM streaming is exactly SSE's shape: client sends one request, server streams tokens one way. OpenAI/Anthropic APIs stream via SSE; the UI reads chunks and renders incrementally.

// browser side of an SSE token stream
const es = new EventSource('/api/chat/stream?id=42');
es.onmessage = (e) => {
  if (e.data === '[DONE]') { es.close(); return; }
  const delta = JSON.parse(e.data).token;
  answerEl.textContent += delta;          // incremental render
};
es.onerror = () => { /* EventSource auto-reconnects with Last-Event-ID */ };

Serverless functions vs an always-on server — and what are cold starts?

Always-on (VM/container/Spring Boot pod) — a process you run, size and pay for 24/7; state in memory possible; predictable latency.

Serverless (Lambda, Vercel/Netlify functions) — the platform spins up an instance per demand, scales to zero, bills per invocation. No server management, but: stateless by force, execution time limits, and cold starts — the first request after idle pays for provisioning + runtime boot (ms for Node, notoriously seconds for JVM — hence GraalVM native images for serverless Java).

Mitigations: provisioned concurrency/warm-up pings, small bundles, lighter runtimes, keeping DB connections in a pooler (serverless + naive connection-per-invocation exhausts Postgres).

What does 99.5% (or 99.9%) uptime actually mean? How do you design for it?

Availability translates directly into an allowed-downtime budget:

  • 99% ≈ 7.3 hours/month
  • 99.5% ≈ 3.6 hours/month
  • 99.9% ≈ 43 minutes/month
  • 99.99% ≈ 4.4 minutes/month

Each nine multiplies cost: redundancy (no SPOF, multi-instance/multi-AZ), health checks + automatic failover, zero-downtime deploys, graceful degradation (serve cached/partial rather than error), and an error budget mindset — planned maintenance and bad deploys spend the same budget.

Monorepo vs polyrepo — trade-offs (especially for microfrontends)?

Monorepo — all apps/libs in one repo (Nx/Turborepo): atomic cross-cutting changes (shared component + all consumers in one PR), one dependency version policy, easy code sharing and refactoring; needs task-graph tooling to build/test only what changed, and CI/permissions get complex at scale.

Polyrepo — repo per app/team: clear ownership and independent pipelines (the natural fit for independently-deployed microfrontends/microservices); the price is version drift, cross-repo changes spanning N PRs, and shared libraries distributed via registries with lagging upgrades.