System Design · revision

System design revision notes

Every lesson's must-know points, design steps, common mistakes and interview answer on one page, for the night before an interview. Each title opens the full lesson.

Phase 0: How system design interviews work

Meet the building blocks: a map of a typical web system

Must know

Most large web systems are built from the same dozen parts — clients, DNS, a CDN, load balancers, app servers, caches, databases, queues, workers and object storage — connected in a familiar shape.

Must-know points

  • Client: the app or browser the user holds.
  • DNS: turns a name (example.com) into a server's address.
  • CDN: servers around the world that keep copies of files close to users.
  • Load balancer: spreads requests across many servers and skips broken ones.
  • App server: runs your code; usually stateless, so you can add more.
  • Cache: fast memory holding copies of often-read data (often Redis).
  • Database: the permanent, trusted store of your data.
  • Queue and workers: a waiting line of jobs, and the background programs that do them.
  • Object storage: cheap, huge storage for files like photos and videos (for example Amazon S3).

Common mistakes

  • Learning tool names (Redis, Kafka) before knowing what job each box does.
  • Thinking every system needs every box. A small app may only need one server and one database.
  • Treating the cache as the place where data really lives. The database is the source of truth.

Interview answer

Most designs start from the same shape. Clients reach us through DNS; files come from a CDN; a load balancer spreads requests across stateless app servers; those servers read from a cache and a database, push slow work onto a queue for workers, and keep large files in object storage. Then I scale whichever box the requirements stress most.

Follow-up questions

Which box is usually the hardest to scale?

The database, because it holds data that must stay correct. App servers are easy to copy; data is not.

Why put a queue between the app server and the workers?

So the user doesn't wait for slow work, and so a burst of jobs waits in line instead of overloading the workers.

The high-level design interview: a step-by-step answer framework

Must know

A repeatable order of steps for answering any 'Design X' question in about 45 minutes.

Must-know points

  • 1. Requirements (about 5 min): functional (what it does) and non-functional (scale, latency, availability, consistency). Write them down.
  • 2. Estimates (about 3 min): users, requests per second, storage, bandwidth. Only as much as changes a decision.
  • 3. API (about 3 min): the main endpoints with inputs and outputs.
  • 4. Data model (about 3 min): the main entities, and which kind of database fits.
  • 5. High-level design (about 10 min): the simplest design that works end to end.
  • 6. Deep dives (10–15 min): fix the bottlenecks — caching, sharding, queues, failure handling.
  • 7. Wrap-up: the trade-offs you made, what you'd do with more time, how you'd monitor it.

Common mistakes

  • Designing before asking a single question.
  • Adding Kafka, microservices and sharding to a system that gets 10 requests a second.
  • Long silences. The interviewer can only give credit for what you say.
  • Never stating trade-offs. Every choice has a cost, and naming it is what earns the strongest marks.

Interview answer

Before I design anything, I'd like to clarify the requirements and the scale. Then I'll propose a simple design that works end to end, and we can go deep on the parts you care about most, discussing the trade-offs as we go.

Follow-up questions

What if they mention a technology you don't know?

Say so honestly, then reason from first principles: describe what the component must do (for example, 'a durable queue that keeps order per user') and the properties you need.

How detailed should the estimates be?

Rough powers of ten are enough. The point is to decide things like 'one database is enough' or 'we need sharding', not to get exact numbers.

Back-of-the-envelope estimation

Must know

Quick, rough maths to estimate traffic, storage and bandwidth, so your design decisions are based on scale.

Must-know points

  • Use powers of ten for speed: KB ≈ 10³ bytes, MB ≈ 10⁶, GB ≈ 10⁹, TB ≈ 10¹², PB ≈ 10¹⁵.
  • Seconds per day ≈ 10⁵. Requests per day ÷ 10⁵ ≈ requests per second.
  • Read-to-write ratio matters: many apps read 10–100 times more than they write, which points to caching and read replicas.
  • Latency ladder: memory read ≈ 100 ns; SSD read ≈ 100 µs; round trip inside a datacentre ≈ 0.5 ms; across a continent and back ≈ 100–150 ms.
  • Storage = items per day × size × days kept × copies (often 3 for replication).
  • Rough capacity: one app server handles thousands of simple requests per second; one SQL database handles thousands to tens of thousands of simple queries per second.

Common mistakes

  • Spending 10 minutes on exact arithmetic. Round hard: 86,400 ≈ 100,000.
  • Using the average and forgetting the peak.
  • Mixing up bits and bytes: network speeds are in bits, so 1 Gbps ≈ 125 MB a second.
  • Working out numbers and then never using them to make a decision.

Interview answer

I'll estimate the scale first, because it decides the design. For example, 100 million requests a day is only about 1,200 per second on average, maybe 5,000 at peak. A few load-balanced app servers and one database with read replicas can handle that. If it were 100 times more, I'd plan for sharding and heavy caching.

Follow-up questions

Why multiply by a replication factor?

Data is usually stored about 3 times for durability and availability, so real disk use is roughly 3 times the raw data.

What if your estimate is wrong?

That's fine. State your assumptions clearly; the interviewer cares that the design changes sensibly when the numbers change.

The low-level design interview: a step-by-step approach

Must know

A repeatable way to answer 'Design the classes for X' (a parking lot, an elevator, Splitwise) and write the key code.

Must-know points

  • 1. Clarify requirements and scope: the main use cases, what's out of scope.
  • 2. Identify entities (nouns) and actions (verbs) from the use cases.
  • 3. Draw a class diagram: fields, methods, relationships (composition, inheritance, interfaces).
  • 4. Apply patterns where behaviour varies: Strategy, State, Factory, Observer.
  • 5. Code the core flows with clean names; skip trivial getters.
  • 6. Discuss concurrency (two users at once), edge cases and extensibility.
  • Use SOLID to justify your choices out loud.

Common mistakes

  • One giant class (a 'god class') that does everything.
  • Deep inheritance trees where composition would be simpler.
  • Using patterns that don't fit, just to mention them.
  • Ignoring concurrency when two users can act on the same thing.

Interview answer

I'll confirm the use cases first, then pull the main entities out of them and sketch a class diagram. Where behaviour is likely to change, like pricing, I'll put it behind an interface. Then I'll code the core flow and finish with concurrency and how the design extends.

Follow-up questions

Which language should I use?

The one you're strongest in. Python and Java are both fine; clean structure matters more than syntax.

How much code is expected?

The main classes and the one or two core flows, written cleanly. Not every getter, setter or menu.

Phase 1: Networking and APIs

Client–server, DNS, and what happens when you type a URL

Must know

Clients (browsers, apps) send requests; servers process them and send responses. DNS turns a name like example.com into an IP address the client can connect to.

Must-know points

  • DNS lookup order: browser cache → operating system cache → resolver (for example your ISP) → root server → top-level domain (.com) → the domain's authoritative server.
  • TCP gives reliable, ordered delivery. UDP is faster but packets can be lost; it suits video calls, games and DNS itself.
  • TLS (the S in HTTPS) encrypts the connection and proves the server's identity with a certificate.
  • Every new connection costs round trips, so clients reuse connections (keep-alive, HTTP/2).
  • IP address = which machine; port = which program on it (443 for HTTPS).

Common mistakes

  • Forgetting DNS caching when you plan a failover by changing DNS records.
  • Mixing up TCP (the reliable connection) with HTTP (the request format that travels over it).
  • Thinking HTTPS only encrypts. It also proves the server's identity.

Interview answer

The browser resolves the domain through DNS, usually from a cache, to get an IP address. It opens a TCP connection and does a TLS handshake, then sends the HTTP request. The request typically hits a load balancer, which forwards it to an app server; the server reads from a cache or database and sends back the response.

Follow-up questions

TCP or UDP for a live video call?

Usually UDP-based protocols. A late video packet is useless, so it's better to skip it than to wait for it to be resent.

Why does HTTPS add latency?

The TLS handshake adds round trips before the first request. Reusing connections and TLS 1.3 reduce the cost.

HTTP and REST API design

Must know

HTTP is the request–response protocol of the web. REST is a style of API design built around resources (nouns) and standard methods (GET, POST, PUT, PATCH, DELETE).

Must-know points

  • Methods: GET reads (safe), POST creates, PUT replaces, PATCH changes part, DELETE removes. GET, PUT and DELETE should be idempotent.
  • Status codes: 2xx success (200, 201), 3xx redirect (301, 302), 4xx client error (400, 401, 403, 404, 409, 429), 5xx server error (500, 503).
  • Pagination: offset (simple, but slow and unstable on big, changing lists) or cursor (fast and stable; preferred at scale).
  • Idempotency key: the client sends a unique key so a retried request isn't processed twice. Essential for payments.
  • Versioning: /v1/ in the path or a header, so old clients keep working.
  • Alternatives: GraphQL (the client chooses the fields it wants) and gRPC (fast, typed calls between services).

Common mistakes

  • Returning 200 OK with an error message hidden in the body.
  • Using GET for actions that change data (crawlers and browser prefetching may call it).
  • No pagination, so one request can return a million rows.
  • Breaking changes without a new version.

Interview answer

I'd design the API around resources with standard methods and status codes, paginate lists with cursors, version it, and require an idempotency key on any create or payment endpoint so client retries are safe.

Follow-up questions

401 or 403?

401 means not authenticated (we don't know who you are). 403 means authenticated but not allowed to do this.

Why is cursor pagination better than offset?

Offset has to skip rows, which is slow for page 10,000, and pages shift when new rows arrive. A cursor continues from the last item seen, so it's fast and stable.

Real-time communication: polling, long polling, WebSockets, SSE

Should know

Ways for a server to get new data to a client quickly: the client keeps asking (polling), waits on an open request (long polling), keeps a two-way connection open (WebSockets), or receives a one-way stream (server-sent events, SSE).

Must-know points

  • Short polling: simple, but most responses are empty; high load, and a delay of up to one polling interval.
  • Long polling: fewer empty responses; works everywhere; one request per message.
  • WebSockets: two-way and low latency, but connections are stateful, which makes load balancing and scaling harder.
  • SSE: one-way, server to client, over normal HTTP; reconnects automatically; good for feeds and notifications.
  • Stateful connections need a 'who is connected where' registry, often in Redis.

Common mistakes

  • Choosing WebSockets for one-way updates, where SSE is simpler.
  • Forgetting that open connections make servers stateful: you need a connection registry and reconnect logic.
  • No plan for messages missed while a client was disconnected.

Interview answer

For chat I'd use WebSockets, because messages flow both ways with low latency. Users connect to a fleet of gateway servers, and Redis maps each user to their gateway so a message can be pushed to the right connection. For one-way updates like live scores, SSE is simpler.

Follow-up questions

What happens when a WebSocket server crashes?

Clients reconnect (with backoff) to another server, re-register where they are, and fetch any missed messages using the last message ID they saw.

Why not just poll every second?

With millions of users, that's millions of mostly empty requests every second: huge cost for little benefit.

CDN (content delivery network)

Must know

A network of servers around the world that caches static content (images, video, scripts) close to users.

Must-know points

  • Best for static, popular, cacheable content: images, video segments, JavaScript and CSS files.
  • Pull CDN: fetches from your origin server on a cache miss. Push CDN: you upload content ahead of time.
  • Control freshness with TTL headers; put a version or hash in file names (app.3f9a.js) so updates appear immediately.
  • Also helps with DDoS protection, and can handle TLS close to the user.

Common mistakes

  • Caching personalised pages at the edge, and showing one user's data to another.
  • Long TTLs on files that change, without versioned names.
  • Forgetting that the first request in each region is still slow (a 'cold' cache).

Interview answer

I'd serve static assets and media through a CDN, so users download them from a nearby edge server. That lowers latency and takes most of the bandwidth off our origin. I'd use versioned file names so updates show up immediately.

Follow-up questions

Can a CDN cache API responses?

Sometimes, for short TTLs on public data like a product page. Personalised responses usually can't be cached at the edge.

Pull or push?

Pull for most sites (simple and automatic). Push when content is large and known in advance, like a video library.

Proxies, reverse proxies and API gateways

Should know

A reverse proxy sits in front of your servers and receives requests on their behalf. An API gateway is a reverse proxy with API features: authentication, rate limiting and routing to the right service.

Must-know points

  • Forward proxy: acts for clients (for example, a company's web proxy). Reverse proxy: acts for servers.
  • Gateway jobs: routing, authentication, rate limiting, TLS, request logging, API versioning.
  • A load balancer spreads traffic across copies of a service; a gateway decides what's allowed and which service it goes to. Many products do both.
  • Keep business logic out of the gateway.

Common mistakes

  • Putting business rules in the gateway, so it slowly becomes a hidden monolith.
  • Running a single gateway instance: a single point of failure.
  • Doing heavy work in the gateway that slows down every request.

Interview answer

I'd put an API gateway at the edge to authenticate requests, apply rate limits and route each path to the right microservice, so services don't each re-implement those concerns. It's replicated, so it isn't a single point of failure.

Follow-up questions

Gateway or load balancer?

A load balancer spreads requests across copies of one service. A gateway handles API concerns like auth, limits and routing between different services. Often you use both.

What's the downside of a gateway?

An extra network hop, and a central component that must scale and stay up.

Phase 2: Core concepts: scale, reliability and speed

Scalability: vertical vs horizontal

Must know

Scalability is a system's ability to handle more load by adding resources.

Must-know points

  • Stateless app servers plus a shared session store = easy horizontal scaling.
  • Databases are the hard part: read replicas for more reads, sharding for more writes.
  • Scale the real bottleneck: measure first (CPU, memory, database, network).
  • Auto-scaling adds and removes servers based on load.
  • Scalability is not speed: a system fast for one user can still collapse under many.

Common mistakes

  • Adding app servers when the database is the real bottleneck.
  • Keeping sessions in server memory, then adding more servers.
  • Sharding far too early: it adds a lot of complexity.

Interview answer

I'd keep the application tier stateless so it scales horizontally behind a load balancer with auto-scaling. State goes into shared stores: sessions in Redis, data in the database. For the database, I'd add read replicas first and shard only when write volume needs it.

Follow-up questions

When is vertical scaling the right answer?

Early on, or for a database that's hard to split. It's simple and modern machines are very large. Just plan the horizontal path for later.

What makes a server 'stateful'?

It keeps data that later requests depend on, like a session in memory or an uploaded file on its local disk.

Availability: nines, SLAs and redundancy

Must know

Availability is the share of time a system works and responds correctly, usually written in 'nines', like 99.9%.

Must-know points

  • Downtime per year: 99% ≈ 3.65 days; 99.9% ≈ 8.8 hours; 99.99% ≈ 53 minutes; 99.999% ≈ 5 minutes.
  • Dependencies in series multiply availability down; parallel copies multiply the chance of failure down.
  • Remove single points of failure: load balancers, databases, DNS, the gateway itself.
  • Spread across availability zones (separate datacentres in one region); for more safety, across regions.
  • Active–passive: a standby takes over. Active–active: all copies serve traffic all the time.
  • SLI = what you measure; SLO = your target; SLA = your contract.

Common mistakes

  • Two copies that share one point of failure (the same rack, the same power supply, the same bad deployment).
  • Forgetting the database, DNS or load balancer when removing single points of failure.
  • Promising 99.99% when one of your dependencies only offers 99.9%.

Interview answer

I'd first agree a target, say 99.9%. Then I'd remove single points of failure: several app servers across availability zones, a replicated database with automatic failover, redundant load balancers and health checks. I'd also remember that every dependency in series lowers the overall availability.

Follow-up questions

Availability or reliability?

Availability: is it up and responding? Reliability: does it do the right thing over time, without errors or data loss? A system can be up but returning wrong answers.

Active–active or active–passive?

Active–active uses all capacity and fails over faster, but must handle conflicting writes. Active–passive is simpler, but the standby sits idle and failover takes longer.

Fault tolerance: timeouts, retries, circuit breakers

Must know

Fault tolerance means the system keeps working, perhaps in a reduced way, when parts of it fail.

Must-know points

  • Always set timeouts on network calls.
  • Only retry safe (idempotent) operations; use exponential backoff plus random jitter to avoid retry storms.
  • Circuit breaker states: closed (normal) → open (fail fast) → half-open (test) → closed.
  • Graceful degradation: lose a feature, not the whole product.
  • Cascading failure: a slow service ties up threads in the services calling it, which then slow down too.
  • Test it: chaos engineering breaks things on purpose, in a controlled way.

Common mistakes

  • No timeouts, or a timeout longer than the caller's own timeout.
  • Retrying operations that aren't safe to repeat, like charging a card.
  • Retrying instantly without backoff, which turns a small outage into a big one.

Interview answer

Every network call gets a timeout. Idempotent calls are retried with exponential backoff and jitter. Calls to non-critical dependencies sit behind a circuit breaker with a fallback, so the core feature keeps working when they fail. That stops one slow service from causing a cascading failure.

Follow-up questions

Why add jitter to retries?

Without it, all clients retry at the same moments and hit the recovering service in synchronised waves.

What's a bulkhead?

Separate pools of threads or connections for each dependency, so one slow dependency can't use them all up.

Load balancing

Must know

A load balancer spreads incoming requests across several servers and stops sending traffic to unhealthy ones.

Must-know points

  • Algorithms: round robin, weighted round robin, least connections, IP or consistent hashing.
  • Health checks remove failed servers automatically.
  • Layer 4: fast and protocol-agnostic. Layer 7: smarter routing and TLS handling, but more work per request.
  • Sticky sessions tie a user to one server; avoid them by keeping servers stateless.
  • The load balancer itself must be redundant (a pair, or a managed service).
  • Load balancers are also used between internal services, not only at the edge.

Common mistakes

  • Sticky sessions that tie one user to a server forever, which breaks when that server dies.
  • Health checks that only test 'is the process running?', not 'can it reach its database?'.
  • Forgetting that the load balancer needs redundancy too.

Interview answer

I'd put a layer 7 load balancer in front of the stateless app servers, with health checks and least-connections routing. It runs as a redundant pair or a managed service, so it isn't a single point of failure.

Follow-up questions

How does a load balancer know a server is down?

Health checks: it calls an endpoint regularly and removes the server after several failures in a row.

When would you route by hashing?

When requests for the same key should reach the same server, for example a cache shard, so the data already in its memory gets reused.

Caching

Must know

A cache keeps a copy of frequently used data in fast storage, usually memory, so it can be served without repeating slow work.

Must-know points

  • Cache-aside (lazy loading): check the cache → on a miss, load from the database and fill the cache. The most common pattern.
  • Write-through: write the cache and database together (fresh, but slower writes). Write-back: write the cache, save to the database later (fast, but data can be lost).
  • Eviction: LRU (least recently used), LFU (least frequently used), TTL expiry.
  • Invalidation: delete or update the cache entry when the data changes; the TTL is your safety net.
  • Problems to know: stale data, cache stampede (many misses at once), hot keys, cold start.
  • Cache only data that is read often and can be slightly out of date.

Common mistakes

  • Caching data that changes constantly, or is hardly ever read.
  • No TTL, so stale data lives forever if one invalidation is missed.
  • No plan for a stampede when a very popular key expires.
  • Treating the cache as the source of truth. It can be emptied at any moment.

Interview answer

Reads far outnumber writes here, so I'd add a Redis cache using cache-aside with a TTL. On writes I'd delete the cache key so the next read loads fresh data. I'd watch the hit rate, and protect hot keys from stampedes by letting one request rebuild while others wait.

Follow-up questions

What is a cache stampede?

A popular key expires and thousands of requests miss at the same moment and all hit the database. Fixes: let one request rebuild while the others wait, add random jitter to TTLs, or refresh early.

Update or delete the cache on a write?

Usually delete. It's simpler and avoids races where two writers leave an old value behind.

Consistency, CAP and PACELC

Must know

Consistency (in distributed systems) means every read sees the latest write. The CAP theorem says that during a network partition, a system must choose between consistency and availability.

Must-know points

  • C = every read sees the latest write; A = every request gets a non-error response; P = the system keeps working when messages between machines are lost.
  • Partitions are unavoidable in distributed systems, so it's CP or AP during a partition.
  • Consistency levels: strong, read-your-own-writes, monotonic reads, eventual.
  • Quorums: with N copies, writing to W and reading from R where W + R > N means every read overlaps the latest write.
  • PACELC: even without partitions, stronger consistency costs latency.
  • CAP's C is not ACID's C (which is about the database's rules and constraints).

Common mistakes

  • Calling a system 'CA'. With replicas, partitions can't be wished away.
  • Treating CAP as one choice for the whole system, instead of a choice per type of data.
  • Confusing CAP's consistency with ACID's consistency.

Interview answer

It depends on the data. For money or stock levels I'd choose strong consistency, accepting that some requests fail during a partition. For feeds, likes or view counts I'd choose availability with eventual consistency, because a slightly stale number is harmless and the system stays up.

Follow-up questions

Is a single-server SQL database CP or AP?

CAP is about distributed systems with replicas. One server has no partition between copies; with replicas, it depends on how replication and failover are set up.

Explain W + R > N.

If every write reaches W copies and every read checks R copies, and W + R > N, at least one copy you read must have the latest write.

Rate limiting and backpressure

Must know

Rate limiting caps how many requests a client can make in a time window. Backpressure is a system telling the senders upstream to slow down when it's overloaded.

Must-know points

  • Token bucket: allows bursts up to the bucket size, with a steady refill rate. The most common.
  • Leaky bucket: processes requests at a fixed rate; smooths out bursts.
  • Fixed window: a simple counter per minute, but allows double bursts at the window edges.
  • Sliding window (log or counter): smoother and fairer, slightly more work.
  • Limit by user, API key or IP address; return 429 with a Retry-After header.
  • Backpressure: bounded queues, load shedding, and signalling clients to slow down.

Common mistakes

  • Limiting per server instead of across all servers.
  • One global limit, so a single heavy user blocks everyone else.
  • Unbounded queues that hide overload until memory runs out.

Interview answer

I'd rate-limit at the API gateway with a token bucket per user or API key, keeping the counters in Redis so every gateway node shares them, and return 429 with Retry-After. Internally I'd use bounded queues and load shedding, so overload slows the system down gracefully instead of crashing it.

Follow-up questions

Why is the fixed window unfair?

A client can send the full limit at 12:00:59 and again at 12:01:00, doubling the real rate across the boundary.

What if Redis is down?

Decide to fail open (allow requests and risk abuse) or fail closed (block and risk an outage), per API. Many systems fail open with a local backup limit.

Asynchronous processing and message queues (Kafka, SQS)

Must know

Asynchronous processing means doing slow work later, outside the user's request. A message queue holds the work, or events, between the service that produces them and the services that consume them.

Must-know points

  • Queue (SQS, RabbitMQ): spreads work; each message is consumed once, then deleted.
  • Log or stream (Kafka): ordered, durable, replayable; partitions give order per key and parallel processing.
  • Delivery guarantees: at most once, at least once (the most common), exactly once (hard; usually 'effectively once' with idempotent consumers).
  • Dead-letter queue: where messages go after failing too many times.
  • Pub/sub: one event fans out to many subscribers.
  • Async means eventual: the user sees 'processing', not the final result straight away.

Common mistakes

  • Assuming each message arrives exactly once. It can arrive twice, so make processing idempotent.
  • Expecting order across a whole Kafka topic. Order is only kept within one partition.
  • Using a queue where the user needs an immediate answer.

Interview answer

Slow or non-critical work goes on a queue so the request stays fast. For simple task distribution I'd use a queue like SQS with a dead-letter queue. For event streams that many services need, or that I may want to replay, I'd use Kafka, partitioned by a key like user ID to keep each user's events in order. Consumers are idempotent, because delivery is at least once.

Follow-up questions

Kafka or SQS?

SQS: a simple managed queue, each message handled once, no replay. Kafka: a high-throughput ordered log with many independent consumers, replay, and ordering within a partition.

How do you keep messages in order?

Send all messages with the same key (like an order ID) to the same partition. Order is only guaranteed within a partition.

Observability: logs, metrics and traces

Must know

Observability is how well you can understand what a system is doing from the outside, using logs, metrics and traces.

Must-know points

  • The four golden signals: latency, traffic, errors, saturation.
  • Use percentiles (p50, p95, p99), not averages; averages hide the slow users.
  • Structured logs (JSON), with a request or trace ID on every line.
  • Distributed tracing (OpenTelemetry) shows the time spent in each service.
  • Alert on SLO burn (user impact), and give every alert a runbook.
  • Health checks and a dashboard per service.

Common mistakes

  • Alerting on causes (CPU at 80%) instead of symptoms users feel (errors, slow responses).
  • Logs without a request or trace ID, so you can't connect them.
  • Watching averages instead of percentiles.

Interview answer

I'd emit the four golden signals as metrics for each service, write structured logs with a trace ID, and use distributed tracing to follow requests across services. Alerts fire on user-facing symptoms, like p99 latency or error rate breaking the SLO, so on-call engineers are paged only for real problems.

Follow-up questions

Why p99 instead of the average?

The average can look fine while 1% of users wait seconds. At scale, that 1% is a lot of people, and often your heaviest users.

Monitoring or observability?

Monitoring checks for known failures with dashboards and alerts. Observability lets you ask new questions about failures you didn't predict.

Phase 3: Data: databases, storage and scaling data

SQL vs NoSQL: choosing a database

Must know

SQL (relational) databases store data in tables with a fixed schema and support joins and transactions. NoSQL is a family of other models: key-value, document, wide-column and graph.

Must-know points

  • Choose by access pattern: what are the main reads and writes, and by which key?
  • SQL: joins, ACID transactions, a strict schema; scales with replicas and, with more effort, sharding.
  • Key-value: very fast by key, no complex queries. Document: flexible schema, nested data.
  • Wide-column (Cassandra): massive write volume; queries designed around the partition key.
  • Graph: relationships first (friends of friends).
  • Default in most interviews: start with SQL unless there's a clear reason not to.
  • Hands-on: the SQL tab teaches the language itself, from SELECT to window functions, with every query runnable.

Common mistakes

  • Choosing NoSQL because it 'scales', without checking the queries you need.
  • Designing a NoSQL schema like SQL tables, then needing joins.
  • Forgetting that different parts of one system can use different databases.

Interview answer

I'd choose based on the access patterns. For orders and payments with relationships and transactions, PostgreSQL. For very high write volume with simple key-based reads, like chat messages, a wide-column store like Cassandra. For sessions and hot data, Redis. Using different stores for different jobs is normal.

Follow-up questions

Why might chat messages go in Cassandra?

Huge write volume, always read by conversation and time: a natural partition key and sort order, with easy horizontal scaling.

Is NoSQL always faster?

No. It's fast for the access pattern it was designed for. Unplanned queries and joins can be slow or impossible.

ACID transactions and isolation levels

Must know

A transaction groups several operations so they succeed or fail together. ACID stands for Atomic, Consistent, Isolated, Durable.

Must-know points

  • Anomalies: dirty reads, non-repeatable reads, phantom reads, lost updates, write skew.
  • Read committed (a common default) stops dirty reads; repeatable read or snapshot stops non-repeatable reads; serializable stops all of them but is slowest.
  • Lost-update fixes: SELECT … FOR UPDATE (pessimistic lock) or a version column (optimistic locking).
  • Durability comes from a write-ahead log saved to disk before the commit is confirmed.
  • Distributed transactions (two-phase commit) exist but are slow and fragile; prefer sagas across services.
  • Hands-on: the SQL tab, Phase 6, runs real BEGIN, COMMIT and ROLLBACK examples.

Common mistakes

  • Checking a balance in one query and updating it in another without a lock: a lost update.
  • Assuming the default isolation level prevents every anomaly.
  • Keeping a transaction open for a long time (for example, while calling another service), which holds locks and blocks others.

Interview answer

For anything involving money or stock I'd use ACID transactions in a relational database. To stop two users buying the last item, I'd use a row lock with SELECT FOR UPDATE or optimistic locking with a version number, and pick the isolation level that blocks the anomalies that matter.

Follow-up questions

Optimistic or pessimistic locking?

Pessimistic locks the row before changing it: safe under heavy contention, but others wait. Optimistic reads a version and only updates if the version hasn't changed: fast when conflicts are rare, with a retry when they happen.

What is write skew?

Two transactions read the same data, each makes a decision, and each writes a different row, together breaking a rule — like two doctors both going off call at once. Serializable isolation prevents it.

Database indexing

Must know

An index is an extra data structure that lets the database find rows without scanning the whole table.

Must-know points

  • B-tree: equality and range queries, and ORDER BY. Hash index: equality only.
  • A composite index on (a, b) helps queries on a, or on a and b, but not on b alone (the leftmost-prefix rule).
  • Covering index: contains every column the query needs, so the table itself isn't read.
  • Every index slows writes and uses storage; index for your real queries only.
  • Use EXPLAIN to check whether a query uses an index.
  • LSM trees (Cassandra, RocksDB): very fast writes, merged in the background.
  • Hands-on: the SQL tab, Phase 7, shows real query plans before and after adding an index.

Common mistakes

  • Indexing every column 'just in case'.
  • A composite index with the columns in the wrong order.
  • Wrapping the column in a function (WHERE LOWER(email) = …) so the index can't be used.

Interview answer

I'd add indexes that match the main queries: for 'latest orders for a user', a composite index on user_id and created_at. I'd check with EXPLAIN, and avoid indexing everything, since every index slows writes.

Follow-up questions

Why doesn't an index on (a, b) help a query on b alone?

The index is sorted by a first, so values of b are scattered all through it — like a phone book sorted by surname being no help for finding a first name.

B-tree or LSM tree?

B-trees update in place and are great for reads. LSM trees append writes and merge later, so writes are faster, but a read may check several files.

Replication

Must know

Replication keeps copies of the same data on several machines.

Must-know points

  • Leader–follower: simple and common; all writes go through one leader.
  • Synchronous: no data loss on failover, but slower writes. Asynchronous: fast, but recent writes can be lost.
  • Replication lag causes stale reads; fixes include read-your-own-writes and monotonic reads.
  • Failover: detect that the leader is dead, promote a follower, redirect writes. Risk: split brain.
  • Multi-leader: writes in several regions; conflicts need rules (last write wins, merging).
  • Leaderless (Dynamo-style): quorums with W + R > N.

Common mistakes

  • Reading from a replica straight after a write, and being surprised by old data.
  • Fully asynchronous replication for data you can't afford to lose.
  • Automatic failover with no protection against two leaders (split brain).

Interview answer

I'd use leader–follower replication: writes to the leader, reads spread across replicas, with at least one synchronous replica in another zone for safe failover. Screens where users must see their own change straight away read from the leader.

Follow-up questions

What is split brain?

Two nodes both believe they're the leader and both accept writes, creating conflicting data. Prevented with consensus, quorums or fencing tokens.

Replication or sharding?

Replication copies the same data to many machines (availability, read capacity). Sharding splits different data across machines (write capacity, storage). Big systems do both.

Partitioning and sharding

Must know

Sharding splits a large dataset across several databases (shards), each holding part of the data.

Must-know points

  • A good shard key: many distinct values, an even spread, and it matches the main access pattern.
  • Hash sharding: even load, poor for ranges. Range sharding: good for ranges, risk of hot spots.
  • Hot keys (a celebrity) can overload one shard: split them further, or cache them.
  • Cross-shard joins and transactions are hard: design so you don't need them.
  • Resharding is painful; consistent hashing or many small logical shards make it easier.
  • Shard only when needed: try caching, read replicas and a bigger machine first.

Common mistakes

  • A shard key with few values (like country), which creates huge, uneven shards.
  • A time-based key that sends every new write to the same shard.
  • Designs whose common requests need joins or transactions across shards.

Interview answer

Once writes or data outgrow one database, I'd shard by a key that matches the main query — here user_id, hashed for an even spread — so a user's data lives on one shard. I'd use consistent hashing or many logical shards to make adding machines easier, and handle very hot users with caching.

Follow-up questions

Why not shard by created_at?

All new writes would land on the newest shard, a hot spot, while the old shards sit idle.

How do you query across shards?

Scatter-gather (ask every shard, merge the results), or keep a separate index or a precomputed view built for that query.

Consistent hashing

Should know

Consistent hashing assigns keys to servers so that adding or removing a server moves only a small share of the keys.

Must-know points

  • hash % N moves most keys whenever N changes.
  • The ring: a key belongs to the first server clockwise from it.
  • Adding or removing a server moves about 1/N of the keys.
  • Virtual nodes even out the load and spread a dead server's keys across many others.
  • Used in: Cassandra, DynamoDB, distributed caches, some load balancers.

Common mistakes

  • Too few virtual nodes, so the load is uneven.
  • Assuming it fixes hot keys. It doesn't.
  • Forgetting replication: each key usually lives on the next 2–3 servers on the ring, not just one.

Interview answer

For a distributed cache or a sharded store I'd use consistent hashing with virtual nodes, so adding or removing a node moves only about 1/N of the keys instead of nearly all of them, and load stays balanced.

Follow-up questions

Why virtual nodes?

With few servers, random ring positions are uneven. Many points per server average this out, and a failed server's keys spread over many servers instead of overloading one neighbour.

What doesn't consistent hashing solve?

Hot keys. One extremely popular key still lands on one server; you need replication or caching for that.

Object storage (S3) for files and media

Must know

Object storage keeps files ('objects') such as images, videos and backups as a key pointing to bytes plus metadata, in a cheap, very durable, almost unlimited store like Amazon S3.

Must-know points

  • Big binary data goes in object storage; the metadata and key go in the database.
  • Pre-signed URLs let clients upload or download directly and securely, for a limited time.
  • Very durable (S3 is designed for 99.999999999%, 'eleven nines') and cheap per GB.
  • Storage classes: hot (frequent access) and cold or archive (cheap, slow to get back).
  • Large uploads use multipart upload: in chunks, retryable and resumable.

Common mistakes

  • Putting files in database BLOB columns.
  • Making a storage bucket public by accident.
  • Sending big uploads through your own app servers.

Interview answer

Files go in object storage like S3, never in the database. The database keeps the object key and metadata. Clients upload directly using pre-signed URLs, and downloads go through a CDN.

Follow-up questions

Why not store images in the database?

It bloats the database, slows backups and replication, and wastes expensive database capacity on cheap bytes.

How do you upload 2 GB over a bad connection?

Multipart upload: send it in chunks, retry the failed ones, and resume instead of starting again.

Search: inverted indexes and Elasticsearch

Should know

Full-text search finds the documents that contain some words and ranks them by relevance. It's built on an inverted index: a map from each word to the documents containing it.

Must-know points

  • Inverted index: each term → a list of document IDs (a posting list).
  • Text analysis: lowercase, split into words, drop stop words, reduce words to their stem.
  • Ranking: TF-IDF or BM25 relevance, plus business signals like popularity.
  • The database stays the source of truth; keep the search index in sync with change events.
  • Search results are eventually consistent with the database.

Common mistakes

  • Making the search engine the source of truth.
  • Forgetting that new or changed items take a moment to become searchable.
  • Using LIKE '%word%' on large tables.

Interview answer

For text search I'd use a search engine like Elasticsearch, built on inverted indexes and fed from the main database through change events. The database stays the source of truth; search can lag by a second or two.

Follow-up questions

Why not just use SQL LIKE?

A LIKE with a leading wildcard can't use a normal index, so it scans every row, and it can't rank results or handle typos.

How do you keep search in sync?

Publish changes, or read them from the database's log (change data capture), onto a queue, and let an indexer update the search engine.

Unique ID generation

Should know

Generating IDs that are unique across many servers, without one shared database counter.

Must-know points

  • Auto-increment: simple, but a single point of failure and a bottleneck, and it reveals your counts.
  • UUID v4: no coordination, 128 bits, random order (bad for inserts into a B-tree index).
  • UUID v7 and ULID: ordered by time and still need no coordination.
  • Snowflake: 64 bits, sortable by time; needs unique machine IDs and sane clocks.
  • Range allocation: each server reserves a block of, say, 1,000 IDs at a time.

Common mistakes

  • Two machines accidentally sharing a machine ID.
  • Ignoring clock problems.
  • Exposing sequential IDs that let outsiders count your orders or guess other users' links.

Interview answer

I'd use Snowflake-style 64-bit IDs: timestamp, machine ID and a per-millisecond sequence. They're generated locally without coordination, they're unique, and they sort roughly by time, which keeps database indexes efficient.

Follow-up questions

Why not UUID v4 everywhere?

128 bits is bigger, and random order scatters inserts across a B-tree index, which hurts write speed. Time-ordered UUID v7 avoids that.

What if the clock goes backwards?

Detect it, and wait until the clock catches up (or refuse to generate), so an ID is never repeated.

Phase 4: Distributed systems and security

Monolith vs microservices

Must know

A monolith is one deployable application. Microservices split the system into small services, each owning one business capability and its own data, talking over the network.

Must-know points

  • Each service owns its data; services don't share a database.
  • They communicate through APIs (synchronous) or events (asynchronous).
  • You'll need: service discovery, an API gateway, distributed tracing, and a deployment pipeline per service.
  • Data consistency across services is eventual; use sagas for multi-step workflows.
  • Conway's law: a system's structure tends to mirror the teams that build it.

Common mistakes

  • Splitting by technical layer (a 'database service') instead of by business capability.
  • Chains of synchronous calls six services deep.
  • A small team starting a new product as 30 microservices.

Interview answer

I'd start with a modular monolith unless there's a clear need for independent scaling or many separate teams. If we split, each service owns its data, services talk through APIs and events, and we invest in a gateway, tracing and automated deployments from day one.

Follow-up questions

What's a 'distributed monolith'?

Services that are split up but so tightly coupled they must be deployed together: all the costs of microservices and none of the benefits.

Why not share one database between services?

It couples them: a schema change for one breaks the others, and no service truly owns its data.

Distributed transactions: sagas and the outbox pattern

Should know

A saga runs a business process across several services as a series of local transactions, with a compensating action to undo each step if a later step fails.

Must-know points

  • Two-phase commit (2PC) gives an atomic commit across databases, but blocks on failures and couples services.
  • Saga = local transactions + compensating actions.
  • Choreography: decentralised, event-driven, but hard to follow as it grows. Orchestration: a central workflow, clearer, one more component.
  • Outbox pattern: avoids the dual-write problem (the database update succeeds but publishing the event fails).
  • Every step and compensation must be idempotent, because messages can arrive twice.

Common mistakes

  • Forgetting compensations, or making them unsafe to repeat.
  • Publishing events outside the database transaction (the dual-write problem).
  • Using two-phase commit across services and creating fragile coupling.

Interview answer

For checkout across services I'd use an orchestrated saga: reserve stock, take payment, confirm the order, with a compensating step for each. Each service writes its state change and its outgoing event atomically using the outbox pattern, and every handler is idempotent.

Follow-up questions

What is the dual-write problem?

Writing to the database and publishing to a queue as two separate steps. If one succeeds and the other fails, the systems disagree. The outbox pattern makes it one transaction.

Why not use two-phase commit?

It holds locks until every participant agrees, and if the coordinator fails, participants can be stuck. It also works poorly across different technologies.

Consensus and leader election (Raft)

Bonus

Consensus is getting several machines to agree on a value — such as who the leader is, or the order of operations — even when some of them fail.

Must-know points

  • Majority (quorum) = more than half; a cluster of N tolerates (N − 1) ÷ 2 failures, so use odd sizes like 3 or 5.
  • Raft roles: follower, candidate, leader. Terms stop an old leader from acting.
  • Randomised election timeouts avoid split votes.
  • Used for: leader election, distributed locks, configuration and metadata.
  • Don't build your own: use etcd, ZooKeeper or a managed service.

Common mistakes

  • Running an even number of nodes.
  • Thinking the smaller side of a partition can still accept writes.
  • Building your own consensus instead of using a tested system.

Interview answer

Where we need a single leader or a distributed lock, I'd rely on a consensus system like etcd or ZooKeeper. It uses Raft or a similar protocol, so a majority must agree and the cluster survives the failure of a minority of nodes.

Follow-up questions

Why odd numbers of nodes?

A 4-node cluster needs 3 for a majority and still only survives 1 failure — the same as 3 nodes, at higher cost.

What is a fencing token?

An increasing number handed out with a lock. Storage rejects writes carrying an older number, so a paused old leader can't corrupt data when it wakes up.

Security basics: authentication, authorisation, OAuth and JWT

Must know

Authentication checks who you are. Authorisation checks what you're allowed to do.

Must-know points

  • Never store plain passwords: use a slow, salted hash (bcrypt, scrypt, Argon2).
  • Session cookies (state kept on the server, easy to revoke) or JWTs (stateless, hard to revoke).
  • OAuth 2.0 = delegated authorisation; OpenID Connect = login and identity on top of it.
  • Authorisation models: role-based (RBAC) or attribute-based (ABAC). Check on every request, on the server.
  • Defence in depth: TLS, input validation, rate limiting, least privilege, audit logs.
  • Common attacks: SQL injection, cross-site scripting (XSS), cross-site request forgery (CSRF), credential stuffing.

Common mistakes

  • Checking permissions only in the user interface.
  • Long-lived JWTs that can't be revoked.
  • Storing passwords with a fast hash (MD5, SHA-256) or without a salt.
  • Secrets committed to Git.

Interview answer

Authentication goes through an identity provider with multi-factor login, issuing short-lived access tokens and refresh tokens. The gateway verifies tokens, and each service enforces authorisation on every request — for example, that the user owns the account. Passwords are stored with a slow hash, everything runs over TLS, and sensitive data is encrypted at rest.

Follow-up questions

How do you revoke a JWT?

Keep them short-lived, revoke the refresh token, and for urgent cases keep a small deny-list that the gateway checks.

Why hash passwords with a slow algorithm?

If the database leaks, attackers have to try guesses one at a time, and a slow hash makes every guess expensive.

Phase 5: Low-level design: OOP and SOLID

OOP: the four pillars

Must know

Object-oriented programming organises code into objects that bundle data and behaviour. Its four pillars are encapsulation, abstraction, inheritance and polymorphism.

Must-know points

  • Encapsulation: private state, with public methods that keep it valid.
  • Abstraction: interfaces and abstract classes describe what, not how.
  • Inheritance: an 'is-a' relationship; reuses code but creates tight coupling.
  • Polymorphism: one interface, many implementations; replaces long if/else chains on type.

Common mistakes

  • Public fields that anyone can set to invalid values.
  • Using inheritance just to reuse a few lines of code.
  • Type checks (if isinstance …) where polymorphism would do.

Interview answer

Encapsulation protects an object's state, abstraction exposes only what callers need, inheritance models an 'is-a' relationship to reuse behaviour, and polymorphism lets code work with an interface while each class supplies its own behaviour — like checkout calling pay() on any payment method.

Follow-up questions

Abstract class or interface?

An interface only declares methods. An abstract class can also hold shared code and state. In Python both are usually written with ABC.

Overloading or overriding?

Overloading: the same method name with different parameters, chosen at compile time (for example in Java). Overriding: a subclass replaces a parent's method, chosen at runtime — that's polymorphism.

Composition over inheritance, and interfaces

Must know

Composition means building a class out of other objects it has ('has-a'), instead of inheriting from a parent ('is-a').

Must-know points

  • 'Is-a' → maybe inheritance. 'Has-a' → composition.
  • Composition lets you change behaviour at runtime and test with fakes.
  • Program to an interface, not to an implementation.
  • Dependency injection: pass collaborators in; don't create them inside the class.

Common mistakes

  • Using inheritance for 'has-a' relationships (a Car extends Engine).
  • Creating collaborators inside the class instead of passing them in.

Interview answer

I prefer composition: a class holds small collaborator objects behind interfaces, and they're injected. It's more flexible than inheritance, easier to test, and avoids fragile class trees. I use inheritance only for a genuine 'is-a' with shared behaviour.

Follow-up questions

What is dependency injection?

Giving an object its dependencies from outside, usually through the constructor, instead of creating them inside — so you can swap them, for example a fake payment gateway in tests.

What is the 'fragile base class' problem?

A change to a parent class unexpectedly breaks subclasses that relied on its internal behaviour.

SOLID: S — Single Responsibility Principle

Must know

A class should have one reason to change: one job.

Must-know points

  • 'A reason to change' usually means a different person or team asking for changes.
  • Warning signs: huge classes, many unrelated imports, methods that never use each other's data.
  • The result: smaller classes that are easier to test.

Common mistakes

  • Splitting by the number of methods instead of by reason to change.
  • Creating dozens of tiny classes that are always changed together.

Interview answer

Single responsibility means each class has one reason to change. For example, I'd separate invoice calculation, saving and emailing, so a change to the email template can't break the tax logic.

Follow-up questions

Isn't that just 'make classes small'?

No, it's about cohesion: things that change together stay together, and things that change for different reasons are separated.

SOLID: O — Open/Closed Principle

Must know

Code should be open for extension but closed for modification: add new behaviour by adding code, not by editing code that already works.

Must-know points

  • Replace 'switch on type' with polymorphism.
  • Strategy, Decorator and plug-in designs all follow this principle.
  • Protects tested code from new changes.

Common mistakes

  • Adding abstractions for variations that will never happen.
  • Still having to edit one central if/else to create the new class. Use a registry or configuration instead.

Interview answer

Open/closed means I can add behaviour without editing existing code — for example, new discount types as new classes behind a DiscountRule interface, instead of a growing if/else chain.

Follow-up questions

Does it mean you never edit code?

No. You still fix bugs and refactor. It means designing so that the variations you expect don't require changing working code.

SOLID: L — Liskov Substitution Principle

Must know

Objects of a subclass must work anywhere the parent class is expected, without breaking the program.

Must-know points

  • Subclasses keep the parent's contract: the same or looser inputs, the same or stronger outputs.
  • Warning signs: overridden methods that throw 'not supported', or isinstance checks in calling code.
  • Fix: a better abstraction, such as a Shape interface with area().

Common mistakes

  • Overriding a method just to throw 'not supported'.
  • A subclass that needs more specific input than its parent accepts.

Interview answer

Liskov means a subclass must work anywhere its parent does. The classic counter-example is Square extending Rectangle: setting width and height separately breaks for a square, so they shouldn't share that inheritance.

Follow-up questions

Penguin extends Bird, and Bird has fly()?

A penguin can't fly, so fly() would throw — a Liskov violation. Fix it with a separate FlyingBird type, or give flying as an ability through composition.

SOLID: I — Interface Segregation Principle

Must know

Clients shouldn't be forced to depend on methods they don't use. Prefer several small, focused interfaces over one big one.

Must-know points

  • Warning sign: methods that raise NotImplementedError or do nothing.
  • Split interfaces by what each client needs (role interfaces).
  • Works hand in hand with single responsibility.

Common mistakes

  • One interface per method everywhere, which fragments the design.
  • Keeping the fat interface 'for convenience'.

Interview answer

Interface segregation means small, client-specific interfaces, so classes don't implement methods they don't need — for example separate Printer and Scanner interfaces instead of one big Machine interface.

Follow-up questions

Interface segregation or single responsibility?

Single responsibility is about why a class changes. Interface segregation is about what clients are forced to depend on. Both lead to smaller, focused pieces.

SOLID: D — Dependency Inversion Principle

Must know

High-level code should depend on abstractions, not on concrete low-level details.

Must-know points

  • Depend on interfaces; inject the implementations (dependency injection).
  • Makes testing easy with fakes and mocks.
  • Lets you swap infrastructure, like the database or email provider, with no change to business logic.
  • Dependency inversion is the principle; dependency injection is a technique for achieving it.

Common mistakes

  • An interface that copies one vendor's API exactly, so the abstraction leaks.
  • Creating dependencies inside methods 'just this once'.

Interview answer

Dependency inversion means business logic depends on interfaces, and concrete implementations are injected. My OrderService depends on an OrderRepository interface, so I can use Postgres in production and an in-memory fake in tests.

Follow-up questions

Dependency inversion or dependency injection?

Inversion is the design rule: depend on abstractions. Injection is the technique: pass dependencies in, often done for you by a framework like Spring.

UML class diagrams and relationships

Should know

A class diagram shows the classes, their fields and methods, and how they relate to each other.

Must-know points

  • Composition (filled diamond): the owner controls the part's lifetime.
  • Aggregation (empty diamond): shared parts that can exist alone.
  • Inheritance: a line with an empty triangle. Realisation: a dashed line with an empty triangle.
  • In an interview, show only the important classes and methods.

Common mistakes

  • Drawing every getter and setter.
  • Spending 20 minutes on the diagram and leaving no time to code.

Interview answer

I start an LLD answer by listing the main entities, then draw a class diagram with the key fields, methods and relationships: composition for owned parts, inheritance only for a true 'is-a', and interfaces for anything likely to vary.

Follow-up questions

Aggregation or composition?

Composition: the part dies with the whole, like a room in a house. Aggregation: the part can exist on its own, like a player in a team.

Concurrency basics: threads, locks and race conditions

Should know

Concurrency is several tasks making progress at the same time. A race condition is a bug where the result depends on the exact timing between them.

Must-know points

  • Critical section: code that touches shared data; protect it with a lock.
  • Race condition: read–modify–write without protection.
  • Deadlock needs four conditions; break 'circular wait' by always taking locks in the same order, or use timeouts.
  • Across several servers, an in-program lock isn't enough: use database transactions or a distributed lock.
  • Prefer thread-safe queues and data that never changes, where you can.

Common mistakes

  • Locking only the write, instead of the whole read-check-write.
  • Holding a lock while doing slow work, like a network call.
  • Taking locks in different orders in different places, which causes deadlock.

Interview answer

Anywhere two users can change the same thing — like booking the last seat — I'd make 'check and update' a single atomic step: a lock within one process, or a database transaction with a row lock or optimistic versioning across servers. I'd take locks in a fixed order to avoid deadlock.

Follow-up questions

Process or thread?

A process has its own memory. Threads share their process's memory, which makes communication easy but needs synchronisation.

How do you prevent deadlock?

Take locks in one agreed global order, hold them briefly, or use try-lock with a timeout.

Phase 6: Design patterns

Singleton

Must know

Makes sure a class has only one instance, and gives one global way to reach it.

Must-know points

  • Use it for a truly single, shared resource.
  • Make creation thread-safe (double-checked locking), or create the instance eagerly.
  • In Python, a module is already a natural singleton.
  • Often criticised as global state; prefer injecting one shared instance.

Common mistakes

  • Using Singleton for things that aren't truly single, like 'the current user'.
  • Hidden dependencies: code reaches for the global instead of receiving what it needs.

Interview answer

Singleton guarantees one instance with one access point, for things like configuration or a connection pool. I'd make creation thread-safe, but I usually prefer creating one instance at start-up and injecting it, because a global singleton hides dependencies and makes testing harder.

Follow-up questions

Why is Singleton sometimes called an anti-pattern?

It's global, changeable state: hidden dependencies, hard to replace in tests, and tricky lifetimes.

Factory: Factory Method and Abstract Factory

Must know

A factory creates objects for you, so the calling code asks for 'a notifier' without knowing which concrete class gets built.

Must-know points

  • Use it when the exact class depends on input or configuration.
  • Factory Method: one product, a method decides. Abstract Factory: families of related products.
  • A registry dictionary avoids long if/else chains.
  • The 'simple factory' (one create function) is the usual interview version.

Common mistakes

  • A factory that's just a giant if/else edited for every new type.
  • Factories for objects that only ever have one type.

Interview answer

A factory centralises object creation: callers ask for a type by name or configuration and get an object behind a common interface, so adding a new type doesn't change the calling code. Abstract Factory extends this to whole families of matching objects.

Follow-up questions

Factory or Builder?

A factory picks which class to create, in one step. A builder assembles one complex object step by step.

Builder

Must know

Builds a complex object step by step, instead of using a constructor with a dozen parameters.

Must-know points

  • Use it for objects with many optional settings, or multi-step construction.
  • Fluent style: each step returns the builder, so calls can be chained.
  • Validate in build(); the finished object can then be unchangeable.
  • In Python, keyword arguments with defaults often solve the same problem; in Java, Builder is very common.

Common mistakes

  • Letting the half-built object escape before build() is called.
  • Using a builder for objects with only two or three fields.

Interview answer

Builder constructs a complex object step by step through readable, named methods, then validates it and returns it, instead of a constructor with many parameters. I'd use it for things like requests or reports with lots of optional settings.

Follow-up questions

Why is Builder less needed in Python?

Keyword arguments with defaults already make optional settings readable. Builder still helps when construction has several steps or needs validation.

Adapter

Must know

Lets two incompatible interfaces work together, by wrapping one so it looks like the other.

Must-know points

  • Use it to plug in third-party or legacy code without changing your own interface.
  • Prefer wrapping an object (object adapter) over inheriting from it (class adapter).
  • Keeps vendor-specific code in one place, so switching vendors is easy.

Common mistakes

  • Letting the third-party's types leak through the adapter into business code.
  • Losing error details during the conversion.

Interview answer

Adapter wraps an incompatible class so it matches the interface my code expects — for example making an old bank SDK fit our PaymentGateway interface — so business code doesn't change and vendors can be swapped.

Follow-up questions

Adapter or Facade?

An adapter makes one interface match another that's expected. A facade gives a simpler interface over a complicated set of classes.

Decorator

Must know

Adds behaviour to an object by wrapping it in another object with the same interface, without changing the original class.

Must-know points

  • Same interface as the wrapped object, so wrappers can stack.
  • Avoids an explosion of subclasses for every feature combination.
  • Follows open/closed: add features without editing classes.
  • Python's @ syntax is a related idea for functions.

Common mistakes

  • Wrappers that change what the method means, rather than just adding to it.
  • Wrapping in the wrong order (for example, caching errors).

Interview answer

Decorator wraps an object with another that has the same interface and adds one behaviour, like logging, caching or retries. Wrappers can be stacked in any combination, which avoids an explosion of subclasses.

Follow-up questions

Decorator or inheritance?

Inheritance fixes behaviour for a whole class when you write the code. Decorators add it to individual objects at runtime, and combine freely.

Facade

Should know

Gives one simple interface to a complicated set of classes.

Must-know points

  • Simplifies a subsystem; the subsystem stays available for advanced use.
  • Reduces coupling between callers and many classes.
  • Keep it thin: coordination, not business logic.

Common mistakes

  • Putting business rules into the facade.
  • Forcing every caller through the facade even when they need fine control.

Interview answer

Facade gives callers one simple method, like place_order(), over a complex subsystem of inventory, payment and shipping classes, which reduces coupling. I keep it thin so it doesn't become a god class.

Follow-up questions

Facade or API gateway?

The same idea at different levels: a facade simplifies classes inside one program; an API gateway simplifies many services for outside clients.

Strategy

Must know

Puts a family of interchangeable algorithms behind one interface, so you can choose one at runtime.

Must-know points

  • Replaces 'which algorithm?' conditionals with polymorphism.
  • Strategies can be swapped at runtime.
  • In Python, plain functions can be strategies.
  • Very common in LLD answers: pricing, payment, parking fees.

Common mistakes

  • Strategies that need lots of the context's private data (a sign the interface is wrong).
  • Creating strategy classes for one fixed algorithm.

Interview answer

Strategy puts each interchangeable algorithm, like normal or surge pricing, behind one interface, so the class using it can switch algorithms at runtime, and new ones are added without editing it.

Follow-up questions

Strategy or State?

Both swap behaviour objects. With Strategy, the caller chooses the algorithm. With State, the object changes its own state as events happen.

Observer

Must know

Lets objects subscribe to another object's events, and get notified automatically when something happens.

Must-know points

  • Decouples the publisher from its listeners.
  • Subscribers can be added and removed at runtime.
  • Watch for memory leaks (forgotten subscribers) and slow or failing subscribers.
  • At system level, the same idea becomes pub/sub with a message broker.

Common mistakes

  • Never unsubscribing, which leaks memory.
  • Letting one subscriber's error stop the others from being notified.
  • Relying on subscribers being called in a particular order.

Interview answer

Observer lets listeners subscribe to an object's events, so the publisher notifies all of them without knowing who they are — like 'order placed' triggering an email and a stock update. Across services I'd use the same idea with a message broker, for durability.

Follow-up questions

Observer or pub/sub?

With Observer, subscribers register directly with the subject, usually inside one program. With pub/sub, a broker sits in between, so publishers and subscribers don't know each other, often across services.

Chain of Responsibility

Should know

Passes a request along a chain of handlers; each one either deals with it or passes it on to the next.

Must-know points

  • Decouples whoever sends the request from whichever handler deals with it.
  • Handlers can be added, removed or reordered.
  • Make sure something handles the end of the chain.
  • Middleware pipelines are the everyday example.

Common mistakes

  • No default at the end, so requests disappear silently.
  • Very long chains that are hard to trace.

Interview answer

Chain of Responsibility passes a request through a sequence of handlers until one deals with it — like expense approvals by amount, or middleware for auth, rate limiting and validation. It keeps each handler small and lets me reorder the chain.

Follow-up questions

Where have you seen it?

Middleware in web frameworks (Express, Django, Spring filters), and events bubbling up through a web page.

State

Should know

Lets an object change its behaviour when its internal state changes, by putting each state's behaviour in its own class.

Must-know points

  • Each state class handles the events that are valid in that state.
  • Transitions are explicit; invalid actions get a clear response.
  • Start by drawing a state diagram.
  • A cousin of Strategy, but here the object changes its own behaviour.

Common mistakes

  • States that know too much about each other.
  • Forgetting some events in some states (like cancel while dispensing).

Interview answer

State moves each state's behaviour into its own class, so a vending machine or an order behaves correctly for its current state and the transitions are explicit, instead of an if/else on a status field in every method.

Follow-up questions

When is an enum and a switch enough?

When there are only a few states and events. The State pattern pays off as the states and rules grow.

Phase 7: Low-level design questions

Design a parking lot

Must knowLLD

Design the classes for a multi-floor car park that assigns spots, issues tickets and charges fees.

The design, step by step

  1. Entry: a vehicle arrives → the assignment strategy finds a free spot that fits → the spot is marked taken in one atomic step → a ticket is issued.
  2. Exit: scan the ticket → the pricing strategy works out the fee from the time parked → payment → the spot is freed.
  3. Display boards subscribe to spot changes (Observer) and update their counts.

Deep dives

  • Concurrency: two gates assigning the same spot. Use a lock (per floor, or for the whole lot), or an atomic compare-and-set on the spot.
  • Fit rules: can a bike use a car spot when bike spots are full? Decide, and say it out loud.
  • Lost ticket: charge the maximum daily fee.
  • Extensibility: electric-car charging spots become a new spot type with their own pricing strategy.

Common mistakes

  • Putting pricing logic inside Ticket or Vehicle.
  • Ignoring two gates acting at the same moment.
  • Vehicle subclasses that override lots of behaviour instead of just declaring their size.

Interview answer

Core classes: ParkingLot, Floor, Spot, the Vehicle types, Ticket and Payment. Spot assignment and pricing are strategies, so the rules can change without touching the rest. Parking is one atomic step — a lock or a compare-and-set — so two gates can't take the same spot. Display boards observe spot changes.

Follow-up questions

How would this work with gates on different servers?

Keep spot state in a database and claim a spot with a conditional update (UPDATE … WHERE spot_id = ? AND vehicle_id IS NULL), trying another spot if no row changed.

How would you add electric-car charging?

A new spot type with a charger, plus a pricing strategy that adds the energy cost. No existing classes change.

Design an elevator system

Should knowLLD

Design the classes and scheduling logic for a building's lifts.

The design, step by step

  1. Each lift serves its stops with the LOOK algorithm: keep going in one direction, serving stops on the way, then turn around — like a lift operator sweeping up and down.
  2. A controller assigns each hall call to the best lift: idle and near, or already moving towards that floor in the same direction.
  3. Each lift has a state machine that enforces safety: doors must be closed before moving.

Deep dives

  • Dispatch cost = distance + stops on the way + a penalty for the wrong direction.
  • Peak times: in the morning rush, send idle lifts back to the ground floor.
  • Concurrency: requests arrive from many buttons at once, so use a thread-safe queue into the controller.
  • Overload sensor: the doors stay open.

Common mistakes

  • First-come, first-served scheduling.
  • No state machine, so 'moving with the doors open' is possible.
  • Over-complicating the dispatch algorithm before the basics work.

Interview answer

A controller receives hall calls and gives each one to the lift with the lowest cost: near, idle, or already heading that way. Each lift serves its stops with the LOOK algorithm — continue in one direction, then reverse. A state machine per lift guarantees the doors are closed before it moves.

Follow-up questions

Why not first come, first served?

The lift would zig-zag between floors. Sweeping in one direction serves more people per trip and avoids starving any floor.

How would you test it?

Simulate time in steps with scripted requests, check the safety rules (never moving with the doors open) and measure the average wait.

Design Splitwise (expense sharing)

Must knowLLD

Design an expense-sharing app where friends split bills and can see who owes whom.

The design, step by step

  1. Add an expense: validate the split (it sums to the total) → each participant now owes the payer their share.
  2. Keep one net balance per user: positive means they're owed money, negative means they owe.
  3. Simplify debts: repeatedly match the biggest debtor with the biggest creditor until everyone is at zero.

Deep dives

  • Store money as whole pence (or Decimal), never as float.
  • Rounding: £10 split three ways is 334 + 333 + 333 pence; give the extra penny to someone in a fixed, fair way.
  • The greedy simplification settles n people in at most n − 1 payments.

Common mistakes

  • Floats for money.
  • Splits that don't add up to the total.
  • Losing the leftover pence when rounding.

Interview answer

Expenses have a payer and a list of splits, created by a split strategy that checks they add up to the total. Each expense updates a net balance per user, stored in whole pence. To settle a group, I greedily match the largest debtor with the largest creditor, which needs at most n − 1 payments.

Follow-up questions

Why not use floats for money?

0.1 + 0.2 isn't exactly 0.3 in binary floating point. Money needs whole minor units (pence) or Decimal.

Is the greedy method optimal?

It always finishes in at most n − 1 payments, which is good. Finding the true minimum is NP-hard in general, so greedy is the practical choice.

Design a vending machine

Should knowLLD

Design a vending machine that sells items, accepts coins and gives change.

The design, step by step

  1. Idle → select an item → ItemSelected → insert money → HasMoney (once paid ≥ price) → Dispensing → give change → Idle.
  2. Cancel while in ItemSelected or HasMoney → refund → Idle.
  3. An item with no stock left → SoldOut for that item.

Deep dives

  • Change-making: the greedy method works for UK coins when you have plenty of each, but with limited coins it can fail even when exact change is possible (60p with one 50p and three 20p coins). Use dynamic programming, or refuse the sale.
  • Keep money in whole pence.
  • Power cut mid-sale: record the transaction before dispensing, so it can be recovered.

Common mistakes

  • An if/else on a status string in every method.
  • Taking money for an item that's sold out.
  • Assuming change can always be made.

Interview answer

I'd model the machine with the State pattern — Idle, ItemSelected, HasMoney, Dispensing, SoldOut — so every action is only valid in the right state. Money is in pence; change is worked out from the coins actually available, and the sale is refused if exact change isn't possible.

Follow-up questions

Why the State pattern here?

Every action (insert, select, cancel) behaves differently in each state; state classes avoid an if/else on a status field in every method.

Is greedy change-making always right?

Yes for UK coins with an unlimited supply, but not with limited coin counts. Then use dynamic programming.

Design an LRU cache

Must knowLLD

Design a fixed-size cache that removes the least recently used item when full, with get and put in O(1).

The design, step by step

  1. A hash map finds a key's node in O(1).
  2. A doubly linked list keeps items in order of use: most recent at the front, least recent at the back.
  3. get: move the node to the front. put: update and move to the front, or insert at the front and remove the back node if over capacity.

Deep dives

  • Dummy head and tail nodes remove the edge cases.
  • Thread safety: one lock around get and put (or several locks for parts of the cache).
  • In Python, OrderedDict with move_to_end does the same in a few lines; know both versions.

Common mistakes

  • A singly linked list, which can't remove a node in O(1).
  • Forgetting to update the order on get.
  • Forgetting to delete the evicted key from the dictionary.

Interview answer

A hash map from key to node gives O(1) lookup, and a doubly linked list keeps items in order of use. get moves the node to the front; put inserts at the front and removes the back node when over capacity. Dummy head and tail nodes keep the code simple.

Follow-up questions

Why doubly linked?

To remove a node from the middle in O(1), you need to reach the node before it.

LRU or LFU?

LRU removes the item used longest ago; LFU removes the item used least often. LFU protects popular items better but is more complex.

Design an order-matching engine (stock exchange)

BonusLLD

Design the core of a stock exchange: an order book that matches buy and sell orders.

The design, step by step

  1. An incoming buy order fills against the cheapest sell orders while their price is at or below the buy price, oldest first at each price.
  2. Any quantity left over rests in the book.
  3. Each stock's book is processed by one thread, in sequence, fed by a queue, so no locks are needed inside the book.

Deep dives

  • Price-time priority: better price first; at the same price, the earliest order first.
  • One thread per stock gives determinism and avoids locks; parallelism comes from different stocks.
  • Log every order before processing it, so the book can be rebuilt by replay after a crash.

Common mistakes

  • Ignoring time priority at the same price.
  • Using locks inside the matching loop instead of processing in sequence.
  • Floats for prices.

Interview answer

Each stock has an order book with bids in a max-heap and asks in a min-heap, first in first out within a price. A matching engine processes one stock's orders in sequence on a single thread: an incoming order fills against the best opposite prices while they cross, and any remainder rests in the book. Every order is logged first, so the book can be rebuilt.

Follow-up questions

Why single-threaded?

Matching must be strictly ordered and repeatable. One thread per stock avoids locks and is very fast.

What is price-time priority?

Better prices match first; at the same price, the order that arrived first matches first.

Phase 8: High-level design questions

Design a URL shortener (like bit.ly)

Must knowHLD

Turn long URLs into short links, like bit.ly/x7Kp2a, that redirect to the original.

The design, step by step

  1. Write: the API generates a unique code and stores code → long URL.
  2. Read: GET /{code} → check the Redis cache → on a miss, read the database → redirect.
  3. Code generation: a distributed counter (ID ranges per server) encoded in Base62, or a hash of the URL cut to 7 characters, with collision checks.
  4. Very popular links can also be cached at the edge.

Deep dives

  • Counter + Base62: no collisions. Each server reserves a block of IDs from a coordinator, so servers don't fight over one counter.
  • Hashing: the same URL always gets the same code (no duplicates), but collisions need check-and-retry.
  • 301 (permanent; browsers cache it, so less load but no analytics) or 302 (temporary; every click reaches you, so analytics work).
  • Analytics: log each click to a queue and aggregate in the background.

Common mistakes

  • Hashing the URL without handling collisions.
  • No caching, for a workload that's 100 times more reads than writes.
  • Using 301 redirects, then wondering why the analytics are empty.

Interview answer

It's a read-heavy key-value lookup. Writes generate a unique 7-character Base62 code from a distributed counter, with ID ranges reserved per server, and store code → URL. Reads go through a Redis cache in front of the database and return a 302, so we can count clicks; click events go onto a queue for background analytics.

Follow-up questions

How do you stop people guessing other people's links?

Use random codes, or scramble the counter with a reversible shuffle, instead of handing out sequential codes.

301 or 302?

302 if you want analytics or may change the target later; 301 if you want browsers to cache the redirect and reduce load.

Design a distributed rate limiter

Must knowHLD

A service that limits how many requests each client can make, enforced consistently across many API servers.

The design, step by step

  1. The limiter runs as middleware in the API gateway.
  2. Rules are loaded from a config store and cached locally.
  3. Counters live in a Redis cluster and are checked and updated atomically with a Lua script (or INCR plus EXPIRE for fixed windows).
  4. Over the limit → 429 with Retry-After.

Deep dives

  • Race conditions: two servers reading and writing the same counter at once → use atomic Redis operations or a Lua script.
  • Multiple regions: a limit per region (slightly inaccurate) or a global store (slower).
  • Redis failure: fail open, with a local in-memory limit as a backup.
  • Hot clients: spread counters across Redis nodes by client ID.

Common mistakes

  • Read-then-write counters with no atomicity.
  • Limiting per server instead of across all servers.
  • No answer to 'what if Redis is down?'.

Interview answer

I'd enforce limits in the API gateway using a token bucket per client and rule, with state in a Redis cluster updated atomically by a Lua script, adding about a millisecond. Rules come from configuration. Requests over the limit get 429 with Retry-After. If Redis is unavailable, the gateway fails open with a local backup limit.

Follow-up questions

Why a Lua script?

It runs the read, the refill and the decrement as one atomic step inside Redis, so two requests can't both take the last token.

How accurate is it across regions?

Per-region limits are fast but can let a client go slightly over the global limit; a global store is accurate but adds cross-region latency.

Design a notification system

Must knowHLD

Send notifications (mobile push, SMS, email) to users reliably, at large scale.

The design, step by step

  1. A notification service checks preferences and rate limits, and fills in the template.
  2. It puts messages onto separate queues per channel (push, SMS, email) and per priority (passcodes ahead of marketing).
  3. Channel workers call the providers (Apple and Google push services, an SMS gateway, an email service).
  4. Retries with backoff; repeated failures go to a dead-letter queue; every status is recorded in a log.

Deep dives

  • De-duplication: an idempotency key per notification, so retries don't send twice.
  • Separate priority queues, so a marketing blast never delays a passcode.
  • Provider failover: a second SMS provider.
  • A per-user rate limit, so nobody gets spammed.

Common mistakes

  • Sending inside the user's request.
  • One queue where marketing messages block passcodes.
  • Retries without idempotency, so users get duplicates.

Interview answer

Producers call a notification service that checks preferences, fills in templates and puts messages on queues per channel and priority. Workers deliver through providers like Apple and Google push services and SMS gateways, with retries, dead-letter queues and idempotency keys to avoid duplicates. Passcodes get a high-priority lane, so campaigns never delay them.

Follow-up questions

How do you avoid sending the same notification twice?

An idempotency key per notification, checked in a store before sending; the worker records 'sent' only after success.

What if a provider goes down?

Retry with backoff, then fail over to a backup provider, and circuit-break the failing one.

Design a chat app (like WhatsApp)

Must knowHLD

One-to-one and group messaging, with delivery receipts and online status.

The design, step by step

  1. Each client keeps a WebSocket connection to a chat gateway server.
  2. Sending: gateway → chat service gives the message a time-ordered ID and stores it, then routes it.
  3. Routing: look up the recipient's gateway in Redis and push to it; if they're offline, keep it stored and send a push notification.
  4. Groups: send a copy to each member (small groups), or have members fetch it (very large groups).
  5. Receipts and presence are small events that travel the same path.

Deep dives

  • Ordering: a sequence number per conversation; clients sort by it.
  • Retries without duplicates: the client creates a message ID, so resending is idempotent.
  • Presence: heartbeats with a TTL in Redis; don't broadcast every status change to everyone.
  • End-to-end encryption: the server only ever stores ciphertext.

Common mistakes

  • Polling at scale.
  • Forgetting offline users.
  • No plan for ordering or duplicates.

Interview answer

Clients hold WebSocket connections to gateway servers, and Redis maps each user to their gateway. The chat service stores every message in a Cassandra-style store partitioned by conversation, with a time-ordered ID, then routes it to the recipient's gateway — or keeps it and sends a push notification if they're offline. Client-generated message IDs make retries safe.

Follow-up questions

How do you handle a group with 100,000 members?

Don't write a copy for every member. Store the message once and let members fetch it, or deliver lazily as they come online.

How does a new phone get old messages?

History is stored on the server by conversation; the device syncs from the last message ID it has.

Design a news feed (like Twitter or Instagram)

Must knowHLD

Show each user a feed of recent posts from the people they follow.

The design, step by step

  1. Fan-out on write (push): when someone posts, add the post ID to each follower's feed list. Reads are fast.
  2. Fan-out on read (pull): build the feed at request time from the followed users' recent posts. Writes are cheap; reads are slow.
  3. Hybrid: push for normal users; for celebrities with millions of followers, pull their posts at read time and merge.
  4. Post content comes from a cache and media from a CDN; a ranking service can reorder the feed.

Deep dives

  • The celebrity problem: pushing to 50 million followers for every post is far too expensive → use the hybrid.
  • Each user's feed list keeps only the latest few hundred post IDs.
  • Skip pushing to inactive users; build their feed when they log in.
  • Ranking: gather candidates → score them with a model → re-rank for variety.

Common mistakes

  • Push-only fan-out that ignores celebrities.
  • Storing whole posts in every feed instead of post IDs.
  • Offset pagination on a feed that keeps changing.

Interview answer

I'd use a hybrid fan-out. When a normal user posts, workers push the post ID into each follower's feed list in Redis, so reading is a fast cache lookup. Celebrity posts aren't fanned out; their recent posts are fetched at read time and merged in. Post bodies come from a cache and media from a CDN.

Follow-up questions

Why not fan out on write for everyone?

A celebrity with 50 million followers would cause 50 million writes per post: slow and wasteful.

How do you paginate a feed that keeps changing?

With a cursor: continue from the last post ID the user saw, so new posts don't shift the pages.

Design a video streaming platform (like YouTube or Netflix)

Must knowHLD

Upload, process and stream videos to millions of viewers.

The design, step by step

  1. Upload: the client gets a pre-signed URL and uploads in parts straight to object storage.
  2. An event on a queue triggers transcoding workers, which cut the video into short segments and encode several resolutions and bitrates (a graph of tasks).
  3. The outputs go to object storage, with a manifest (HLS or DASH) listing the segments for each quality.
  4. Playback: the player fetches the manifest and segments from a CDN and switches quality as bandwidth changes (adaptive bitrate).
  5. Video details live in a database with a cache; view counts are aggregated in the background.

Deep dives

  • Adaptive bitrate: small segments (2–10 seconds) at several qualities.
  • Transcoding runs in parallel per segment, with retries per task.
  • Popular videos sit on the CDN; rarely watched ones are served from origin.
  • Resumable uploads for big files.

Common mistakes

  • Streaming from your own servers instead of a CDN.
  • Transcoding inside the upload request.
  • One quality for every device and connection.

Interview answer

Uploads go straight to object storage with pre-signed URLs. A queue triggers transcoding workers that encode short segments at several bitrates and write HLS or DASH manifests. Viewers stream the segments from a CDN with adaptive bitrate. Video details live in a database behind a cache, and view counts are aggregated in the background.

Follow-up questions

Why split videos into segments?

Segments can be transcoded in parallel, cached by the CDN, and let the player switch quality in the middle of a video.

How do you count views at this scale?

Send view events to a stream, aggregate them in batches, and update the stored total periodically.

Design a ride-hailing app (like Uber)

Should knowHLD

Match riders with nearby drivers in real time, track trips and charge fares.

The design, step by step

  1. A location service takes driver updates and keeps only the latest position in an in-memory geospatial index (geohash cells, for example Redis GEO).
  2. A matching service searches nearby cells, ranks drivers by arrival time, and offers the trip to one driver at a time with a timeout.
  3. The offered driver is locked with an atomic status update, so they can't be double-booked.
  4. A trip service tracks the trip's state; pricing (including surge) and payment services handle the money.

Deep dives

  • Geohash: nearby places share a prefix; search the rider's cell and its neighbours.
  • Location updates are short-lived: keep only the latest in memory, and stream the history to cheap storage for analytics.
  • Surge pricing from supply and demand in each area.
  • Driver assignment uses an atomic compare-and-set.

Common mistakes

  • Scanning every driver for every request.
  • Saving every location update permanently in the main database.
  • Offering one ride to several drivers without locking.

Interview answer

Drivers stream their location to a location service that keeps only the latest position in an in-memory geospatial index. A ride request searches nearby cells, ranks drivers by arrival time and offers the trip to one driver at a time, locking that driver atomically to prevent double-booking. Trips and payments live in a transactional database.

Follow-up questions

What is a geohash?

A short string that encodes latitude and longitude as nested grid cells. Nearby places share a prefix, so 'nearby' becomes a prefix lookup plus the neighbouring cells.

Why not save every location update in a database?

That's 250,000 writes a second of data that's out of date within seconds. Keep the latest in memory and stream the history to cheap storage.

Design a payment system

Must knowHLD

Process payments reliably: charge customers, record every money movement, and never lose money or charge twice.

The design, step by step

  1. The payment service receives the request with an idempotency key; a unique constraint in the database blocks duplicates.
  2. It creates a PENDING payment record, then calls the payment provider. The provider handles the card details, so you never store raw card numbers.
  3. The provider's result (response or webhook) updates the status, and balanced double-entry ledger rows are written in the same transaction.
  4. The order service is told through an outbox event.
  5. A nightly reconciliation job compares the ledger with the provider's settlement files.

Deep dives

  • Idempotency keys plus unique constraints make every retry safe.
  • Double-entry ledger: every movement is a debit and an equal credit; balances are sums; entries are never edited, only added (corrections are new entries).
  • Provider timeout: the result is unknown, so retry with the same idempotency key or ask the provider for the status before doing anything else.
  • Store money as whole minor units (pence) together with the currency.

Common mistakes

  • Retrying a timed-out charge with a brand-new request.
  • Floats for money.
  • Updating balances in place, with no ledger history.

Interview answer

Correctness first. Every payment request carries an idempotency key enforced by a unique constraint. The payment service records a pending payment, calls the payment provider — which holds the card data, so we never store card numbers — and on the result writes balanced double-entry ledger rows in one ACID transaction. Events go out through an outbox, and daily reconciliation against the provider catches any mismatch.

Follow-up questions

The call to the provider timed out. Did the customer pay?

We don't know. Never retry with a new request: retry with the same idempotency key, or ask the provider for the payment's status first.

Why double-entry?

Every debit has a matching credit, so the books always balance, mistakes show up as imbalances, and the history is a complete audit trail.

Design search autocomplete

Should knowHLD

Suggest the top completions as the user types into a search box.

The design, step by step

  1. Serving: an in-memory trie (or a prefix → top-k map in Redis); a lookup just walks the prefix.
  2. Data pipeline: log searches → count them every hour or day → rebuild the trie → swap the new one in.
  3. Client: wait for a short pause in typing before asking, and cache recent prefixes; cache common prefixes at the edge.
  4. Split the trie across servers by prefix when it gets too big.

Deep dives

  • Store the top-k completions at every node, so a query never has to search a whole subtree.
  • Freshness: trending terms through a streaming pipeline, with older counts decaying over time.
  • Filter out offensive suggestions.
  • Personalisation: blend global suggestions with the user's own history.

Common mistakes

  • Searching the whole subtree on every keystroke.
  • Rebuilding the trie inside the serving path.
  • Sending a request on every keystroke, with no pause.

Interview answer

Suggestions are served from an in-memory trie where each node already stores its top-k completions, so a lookup is just walking the prefix. Search logs feed a background job that rebuilds the trie regularly. Clients wait for a pause in typing and cache results, and popular prefixes are cached at the edge.

Follow-up questions

Why store the top-k at every node?

Otherwise each request would have to search the whole subtree under the prefix, which is far too slow.

How do you handle a sudden trending term?

A streaming job updates counts with time decay and patches hot prefixes between full rebuilds.

Design a real-time leaderboard

Should knowHLD

Show live rankings of players by score, including each player's own rank.

The design, step by step

  1. Score updates go to the game service, which saves them durably and updates a Redis sorted set.
  2. Top N: ZREVRANGE 0 N−1. A player's rank: ZREVRANK. Both run in logarithmic time.
  3. A separate sorted set per period (daily, weekly), with expiry.
  4. At very large scale: split by score range, or give approximate ranks.

Deep dives

  • Sorted sets are skip lists: logarithmic updates and rank lookups.
  • Ties: fold a timestamp into the score, so whoever reached it first ranks higher.
  • Rebuild Redis from the database after a failure.

Common mistakes

  • Recalculating ranks with SQL on every request.
  • No durable copy, so a Redis restart loses the scores.
  • Ignoring ties.

Interview answer

I'd use a Redis sorted set with the player ID as the member and the score as the sort value: ZINCRBY to update, ZREVRANGE for the top N, and ZREVRANK for a player's rank — all logarithmic. Scores are also saved durably, so the set can be rebuilt. Each time period gets its own set.

Follow-up questions

Why not SQL with ORDER BY score?

Sorting 10 million rows on every request is slow; a sorted set stays in order as scores change.

How do you break ties?

Fold a tie-breaker into the score, for example score × 10¹⁰ minus the timestamp in seconds, so earlier players rank higher.

Design a distributed cache

Should knowHLD

A shared in-memory key-value cache, like Redis or Memcached, spread across many machines.

The design, step by step

  1. Clients, or a proxy, route each key to a node with consistent hashing and virtual nodes.
  2. Each node: an in-memory hash table with LRU eviction and TTL expiry.
  3. Each shard has a replica for failover.
  4. Cluster membership comes from a configuration service, or nodes gossip with each other.

Deep dives

  • Hot keys: replicate them, or add a small local cache inside the app.
  • Cache stampede: let one request rebuild a key while the others wait.
  • Node failure: promote the replica; the misses fall on the database, so warm the cache gradually.
  • Optional persistence (snapshots or an append-only file in Redis).

Common mistakes

  • hash % N routing, which causes mass misses whenever servers change.
  • No plan for hot keys.
  • No warm-up after a failure.

Interview answer

Keys are spread across nodes with consistent hashing and virtual nodes, so adding a node moves only a small share of the keys. Each node is an in-memory hash map with LRU eviction and TTLs, replicated for failover. Hot keys get extra replicas or a local cache, and stampedes are prevented by letting one request rebuild a key.

Follow-up questions

Redis or Memcached?

Memcached: simple, multi-threaded, strings only. Redis: rich data structures, persistence options, replication and clustering.

What happens when a node dies?

Its replica is promoted. Without replicas, its keys become misses that hit the database until the cache warms up again.

Phase 9: AI and ML system design

Design a RAG assistant over company documents

Must knowAI/ML

A chat assistant that answers employees' questions from the company's own documents, with sources.

The design, step by step

  1. Ingestion: parse → clean → split into chunks with some overlap → embed → store in a vector store with permission metadata, triggered whenever a document changes.
  2. Query: rewrite the question using the conversation → hybrid retrieval (vectors plus keywords), filtered by the user's permissions → re-rank → build a prompt with the top chunks → the LLM streams an answer with citations.
  3. Guardrails: check retrieved text for prompt injection, and check the output.
  4. Observability: trace every step; run evaluations on a golden set; collect user feedback.

Deep dives

  • Permissions are enforced in the retrieval filter; never rely on the LLM to hide data.
  • Chunking and re-ranking usually bring the biggest quality gains.
  • Evaluation: retrieval recall@k plus answer faithfulness on a golden set, run as regression tests for every change.
  • Cost and latency: cache frequent answers; send easy questions to a smaller model.

Common mistakes

  • No evaluation, so changes are judged by feel.
  • Handling permissions by asking the LLM not to reveal things.
  • Never updating the index when documents change.

Interview answer

Two pipelines. Ingestion parses, chunks and embeds documents into a vector store with permission metadata, updated whenever a document changes. At query time we rewrite the question, run hybrid retrieval filtered by the user's permissions, re-rank, and give the top chunks to the LLM, which streams a cited answer. Every request is traced, and quality is tracked against a golden evaluation set.

Follow-up questions

How do you stop it leaking documents a user shouldn't see?

Enforce permissions in the retrieval filter, so forbidden chunks are never put into the prompt at all.

Answers got worse after a change. How do you find out why?

Run the evaluation set to see whether retrieval recall or answer faithfulness dropped, then read the traces of the failing questions.

Design a recommendation system

Must knowAI/ML

Recommend items (videos, products, posts) that each user is likely to engage with.

The design, step by step

  1. Two stages. Candidate generation retrieves about 500–1,000 items quickly (embedding similarity with approximate nearest-neighbour search, items often bought together, popular items). Ranking then scores those candidates with a richer model using user, item and context features.
  2. Re-ranking applies business rules: variety, freshness, removing items already seen.
  3. Models train offline on logged interactions; features come from a feature store, defined the same way for training and serving.
  4. Online A/B tests measure the real impact of every change.

Deep dives

  • Cold start: new users get popular and contextual items; new items use content features.
  • Feedback loops and popularity bias: add some exploration.
  • Training-serving skew: one feature definition used both offline and online.
  • Metrics: offline (recall@k, NDCG) and online (click-through, watch time, retention).

Common mistakes

  • Scoring every item with a heavy model on every request.
  • Judging success only by offline metrics.
  • Ignoring the cold-start problem.

Interview answer

A two-stage system: candidate generation pulls about a thousand items cheaply using embedding similarity and simple sources, then a ranking model scores them with user, item and context features from a feature store. A final re-ranking step applies variety and business rules. Models train offline on interaction logs, and every change is checked with an online A/B test.

Follow-up questions

Why two stages?

Scoring millions of items with a heavy model on every request is too slow. A cheap retrieval step narrows the pool, so the expensive model only scores hundreds.

How do you handle a brand-new user?

Popular and trending items, context such as country and device, then adapt quickly from their first clicks.

Design a real-time fraud detection system

Must knowAI/ML

Score every card transaction for fraud within milliseconds, and block or flag the risky ones.

The design, step by step

  1. Transaction → scoring service: fetch real-time features (how many transactions in the last 10 minutes, distance from usual location, device) and historical features → rules engine plus an ML model (often gradient-boosted trees) → decision using thresholds.
  2. A streaming pipeline (Kafka plus a stream processor) keeps the windowed counts and sums up to date.
  3. Every decision and its features are logged; fraud labels arrive later (chargebacks) and are joined for retraining.
  4. A case-management tool for fraud analysts.

Deep dives

  • Class imbalance: choose the precision–recall trade-off from the cost of fraud versus the cost of blocking good customers.
  • Delayed, noisy labels, and fraudsters changing tactics (drift): monitor and retrain often.
  • If the model service is down or slow, fall back to rules only.
  • Point-in-time correct features, so no future information leaks into training.

Common mistakes

  • Using accuracy as the metric.
  • Features calculated with information from the future.
  • No fallback when the model service fails.

Interview answer

Each transaction hits a low-latency scoring service that pulls real-time features from a feature store kept fresh by a streaming pipeline, runs rules plus a gradient-boosted model, and returns allow, verify or block within about 100 milliseconds. Thresholds come from the business cost of fraud versus false declines. Decisions are logged, chargeback labels are joined later, and the model is monitored for drift and retrained regularly.

Follow-up questions

Why not just maximise accuracy?

With 0.1% fraud, a model that always says 'not fraud' is 99.9% accurate and useless. Use precision, recall and cost-weighted metrics.

What if the model service is slow?

Time out and fall back to rules, with a safe default for each risk level, so payments never stall.

Design an LLM serving platform

Should knowAI/ML

Serve large language models to many applications with low latency and cost.

The design, step by step

  1. A gateway handles authentication, quotas, rate limits and routing to a pool of servers per model.
  2. Model servers (vLLM-style) use continuous batching and a paged KV cache to keep the GPUs busy.
  3. GPU pools scale automatically on queue length and latency.
  4. Responses stream back through the gateway; prefix caching reuses work for repeated system prompts.
  5. Metrics: time to first token, tokens per second, GPU utilisation, errors, and usage per team.

Deep dives

  • Prefill (reading the prompt) and decode (generating one token at a time) have different bottlenecks.
  • Continuous batching adds new requests between decode steps instead of waiting for a whole batch to finish.
  • Quantised, smaller or distilled models for cheaper tiers; speculative decoding for speed.
  • Overload: a bounded queue, and dropping low-priority traffic first.

Common mistakes

  • Handling one request at a time on an expensive GPU.
  • Ignoring KV-cache memory when planning capacity.
  • No per-team quotas, so one team can starve the others.

Interview answer

A gateway handles auth, quotas and routing to a GPU pool per model. Each replica runs an inference server with continuous batching and a paged KV cache to keep the GPUs busy, and streams tokens back. Pools scale on queue length and latency, and we track time to first token, tokens per second and cost per team. Quantisation, prefix caching and speculative decoding cut cost and latency.

Follow-up questions

Why is batching hard for LLMs?

Requests have different lengths and finish at different times. Continuous batching swaps finished sequences out and new ones in at every step, instead of waiting for the whole batch.

What limits how many requests one GPU can serve at once?

Mostly KV-cache memory, which grows with sequence length and with the number of requests in flight.

Design an ML training and feature pipeline

Should knowAI/ML

The platform that turns raw data into trained, deployed and monitored models, again and again.

The design, step by step

  1. An orchestrated pipeline (for example Airflow): ingest → validate the data → build features → train → compare with the current model → register.
  2. A feature store: one definition per feature, an offline store for training (with point-in-time correct joins) and an online store for fast serving.
  3. Deploy with shadow or canary releases; monitor data drift, prediction drift and business metrics.
  4. Retrain on a schedule, or when drift is detected.

Deep dives

  • Training-serving skew: compute each feature once and use it in both places.
  • Point-in-time joins stop future data leaking into training.
  • Data validation catches broken upstream data before it trains a bad model.
  • Lineage: every model links to its data version, code commit and settings.

Common mistakes

  • Different feature code in training and in serving.
  • No data validation.
  • No way to roll back a bad model.

Interview answer

An orchestrated pipeline validates data, builds features, trains, compares against the production model and registers the winner with full lineage. A feature store keeps one definition per feature for both training (point-in-time correct) and serving (low latency). New models roll out through shadow or canary releases, are monitored for drift, and are retrained on a schedule or on a trigger.

Follow-up questions

What is training-serving skew?

Features computed differently in training and in production, so the live model sees different inputs from the ones it learned on.

Shadow or canary deployment?

Shadow runs the new model on live traffic without using its answers. Canary lets it make a small share of real decisions, and expands if the metrics hold.