Backend Interview Questions, Answered

20 questions · ~15 min · Last verified August 6, 2026

20questions
~15min total
06topics
0/20known
Practice as flashcards →Self-graded · progress saved on this device

HTTP & APIs

What happens between typing a URL and the page rendering?junior

Most of the wall clock goes to connection setup, not your server code: DNS, TCP, and TLS all cost round trips before a byte of HTTP is sent.

  1. DNS resolutionbrowser cache first, then a recursive resolver
  2. TCP connect + TLS negotiationround trips
  3. HTTP request goes outa load balancer routes it to an application server
  4. server hits databases and caches, returns the response
  5. browser parses HTML, discovers CSS, JS, and image URLs
  6. follow-up requests reuse the connectionkeep-alive, or multiplexed over one HTTP/2 stream
  7. rendering starts before everything has arrived

The interview signal is knowing where the time goes: connection setup is round trips, which is why CDNs and TLS session resumption matter, and the server's own work is often a minority of the total. It also explains why long-lived patterns like SSE reuse one connection instead of polling.

Go deeper: How to Stream LLM Responses: SSE vs WebSockets (and the Fetch-Stream Option)

Which HTTP methods are idempotent, and why does it matter?junior

GET, HEAD, OPTIONS, PUT, and DELETE are idempotent: repeating the request leaves the server in the same state as sending it once. POST is not, and PATCH is not guaranteed to be.

It matters because retries are unavoidable. A client that times out cannot know whether the server acted, so proxies, SDKs, and load balancers only auto-retry methods that are safe to repeat. Retrying a POST can double-charge a customer.

The production fix is making POST idempotent yourself with an idempotency key:

http
POST /payments HTTP/1.1
Idempotency-Key: 4f9d2c1a-order-8841

The server stores the key with the first response and replays that response on duplicates. That is how Stripe survives client retries, and it is the answer interviewers want beyond the definition.

When would you choose REST, RPC, or GraphQL for a new API?mid

Choose based on who calls the API, not fashion. Each option earns its place with a specific caller shape.

REST

The default for resource-shaped public APIs: cacheable at the HTTP layer, every tool understands it, conventions settled. Its failure mode is chatty endpoints and over-fetching.

RPC (usually gRPC)

Service-to-service inside your own infrastructure, where you control both ends and want typed contracts, code generation, and low latency over HTTP/2. Awkward from browsers.

GraphQL

Many differently shaped clients on one data graph: classically mobile, web, and a partner API that each need different fields, while you drown in bespoke endpoints. Gives up HTTP caching.

The trade-offs bite in operations: GraphQL needs query cost limits or one client can write an accidental denial of service. Default to REST and move only when the callers force it.

How do you version an API without breaking existing clients?mid

Version only when you must, and design so you rarely must. Additive changes (new optional fields, new endpoints) are safe as long as clients tolerate unknown fields, so make that tolerance an explicit part of the contract.

Breaking changes need a version signal. A URL prefix like /v2/ is the pragmatic default: visible in logs, trivial to route, impossible to send by accident. Header versioning is cleaner in theory but invisible and easier to get wrong.

The real work is the deprecation process, not the scheme:

  1. run v1 + v2 side by side
  2. log per-client version usageknow who is still on v1
  3. announce a sunset datewarning headers
  4. remove at zero trafficnot at the deadline

Most versioning failures are really removal-without-measurement failures.

Databases

How does a database index work, and when does it hurt?junior

An index is a sorted structure, almost always a B-tree, that lets the database jump to matching rows instead of scanning the whole table. The lookup is a logarithmic descent plus a short range walk, not a read of every page. That is why WHERE email = ? on an indexed column stays at milliseconds on a hundred million rows.

The cost lands on writes: every INSERT, UPDATE, and DELETE must also maintain every index on the table. A write-heavy table with eight indexes pays eight extra writes each time.

An index also hurts when:

  • It is not selective: indexing a boolean buys nothing, the planner scans anyway.
  • It does not match the query shape: a composite index on (a, b) cannot serve a filter on b alone.

The habit that signals experience is checking EXPLAIN before and after, not adding indexes on faith.

Go deeper: Hybrid Search in Postgres: Full-Text + pgvector in One Query, No New Infrastructure

What is the N+1 query problem and how do you fix it?junior

N+1 means one query fetches a list, then one more query runs per row: 1 + N round trips. An ORM loading 100 orders and lazily fetching each order's customer issues 101 queries, and the page that felt fast in dev with 10 rows dies in production with 10,000.

The fix is fetching the association in bulk, either a join or a second query with IN:

sql
SELECT * FROM customers
WHERE id IN (/* customer_ids from the first query */);
  1. measure queries per requestdetectAPM counts, or a query budget asserted in tests
  2. find the lazy load behind the countN+1s hide in innocent-looking code
  3. eager-load the associationfixincludes in Rails, select_related in Django, JOIN FETCH in JPA

The senior point is detection: you catch N+1s by watching query counts, not by reading code.

Why do databases need connection pooling?mid

Because connections are expensive and finite. Each Postgres connection is a forked process holding memory; opening one costs a TCP handshake, TLS, and auth. A service that opens a connection per request will exhaust max_connections long before it saturates CPU.

A pool keeps a small set of warm connections and hands them out per query or per transaction. The numbers matter in interviews: a pool of 20-30 usually outperforms one of 500, because the database spends its time working instead of context-switching.

The production wrinkle is serverless: a thousand concurrent lambdas each with a "small" pool is still a thousand pools. That is why proxies like PgBouncer or RDS Proxy sit in front and pool centrally.

What do transaction isolation levels actually trade off?senior

Isolation levels trade correctness anomalies for concurrency. Each step up removes an anomaly and costs you something under contention.

  • Read committed (the Postgres default) allows non-repeatable reads: two reads inside one transaction can see different data.
  • Repeatable read gives a stable snapshot but still allows write skew: two transactions each read, decide, and write disjoint rows whose combination breaks an invariant, the classic on-call scheduling bug.
  • Serializable prevents that by aborting one of them, which means your code must catch serialization failures and retry.

In MVCC databases the cost of stronger levels is not slower reads, it is aborted work and retry logic under contention. Most systems run read committed and protect the few invariants that matter explicitly:

sql
SELECT * FROM shifts WHERE day = $1 FOR UPDATE;

Escalate a specific transaction to serializable when you must, not the whole database.

Caching

What are the main caching strategies, and where does invalidation bite?mid

Cache-aside is the default: the app checks the cache, misses to the database, and writes the result back with a TTL. The other strategies move that work around:

  • Write-through updates cache and database together, keeping reads fresh at the cost of write latency.
  • Write-behind buffers writes in the cache and flushes later: fast and dangerous, because you can lose acknowledged writes.
  • Read-through moves the miss logic into the cache layer itself.

Invalidation bites wherever data changes outside the code path that populated the cache: a bulk update job, a second service writing the same table, a manual SQL fix during an incident. Explicit invalidation always misses one of those paths eventually.

The senior habit is setting a TTL even when you invalidate explicitly, so a missed invalidation becomes a bounded staleness bug instead of a permanent one.

Go deeper: System Design track (in development)

Where should you cache: CDN, application, or database layer?junior

Cache as close to the user as the data's freshness allows.

Where does this data belong?

  • shared + addressable by URLCDNstatic assets, images, public API responses: the request never touches your infrastructure
  • per-user or computedapplication layerRedis or in-process memory: sessions, rendered fragments, expensive aggregations the CDN cannot key on
  • repeated heavy queriesdatabase layerbuffer pool, materialized views: still costs a network hop and query execution

The trade-off is leverage versus control: the CDN absorbs the most traffic but gives the least invalidation precision, so purges are coarse and mistakes are public.

In practice you layer all three, deciding per data class with two questions: who shares this response, and how stale can it safely be?

What is a cache stampede and how do you prevent it?senior

A stampede is when a popular key expires and every concurrent request misses at once, so hundreds of workers recompute the same expensive value and hit the database simultaneously. The cache was the thing protecting the database, and its expiry removes that protection at the worst possible moment: slow queries pile up, connections exhaust, and the outage spreads.

Three defenses stack:

  1. request coalescingsingle flightonly the first miss recomputes; the others wait or serve the old value
  2. stale-while-revalidatekeep serving the expired value while one background refresh runs
  3. probabilistic early expirationrequests may refresh slightly before the TTL, spreading recomputation over time

Also jitter your TTLs. Warming a fleet's cache at deploy time with identical TTLs is how a system schedules its own synchronized stampede for a day later.

Go deeper: System Design track (in development)

Queues & async work

When should a request become a background job?junior

Queue the work when the user does not need the result to continue, or the work cannot reliably finish inside a request timeout.

Should this request become a background job?

  • user only needs an ackqueue itwelcome emails, reports, image resizing, third-party syncs: return 202 with a job ID
  • may outlive the request timeoutqueue ita 30-second load balancer timeout kills a long export mid-flight
  • user needs the answer nowkeep it inlinedo not queue trivial work

The forcing functions in production are timeouts and retries: a user refreshing a slow page silently triggers duplicate work. A queue buys retries with backoff, rate control against flaky third parties, and isolation so slow tasks never hold web workers hostage.

The cost is a new set of failure modes: jobs need idempotency, monitoring, and a dead-letter path.

At-most-once, at-least-once, exactly-once: what can you actually get?senior

Over a network, true exactly-once delivery is not achievable. You get at-most-once (fire and forget, messages lost on failure) or at-least-once (acknowledge after processing, messages duplicated on failure). When an ack is lost, the sender cannot distinguish "processed" from "never arrived" and must choose to retry or not.

What you can build is exactly-once processing: at-least-once delivery plus idempotent consumers. Either the operation is naturally idempotent (SET status = 'shipped'), or you deduplicate by recording processed message IDs in the same transaction as the side effect.

Systems advertising "exactly-once semantics", like Kafka transactions, implement precisely this inside their own boundary. The moment your consumer emails a customer or calls Stripe, you are back to designing idempotency yourself. Interviewers want that boundary stated plainly.

Go deeper: System Design track (in development)

What problem does the outbox pattern solve?mid

The outbox pattern solves the dual-write problem: you commit a database change and publish an event to a broker, and no transaction spans both. Commit, then crash before publishing, and downstream systems never hear about the order. Publish first, then roll back, and they hear about an order that does not exist.

The outbox makes the event part of the database transaction:

sql
BEGIN;
INSERT INTO orders (...) VALUES (...);
INSERT INTO outbox (topic, payload)
VALUES ('order.created', '{...}');
COMMIT;
  1. order row + outbox row commit togetheratomic
  2. a relay reads the outboxa poller, or change data capture like Debezium
  3. relay publishes to the broker, marks rows as sentat-least-once

Consumers must be idempotent (delivery is at-least-once), and the table needs pruning. You trade a little publish latency for the guarantee that the event exists if and only if the commit did.

Auth & security

Sessions vs JWTs: how do you choose?mid

Server-side sessions are the right default for a first-party web app; JWTs earn their place where the verifier cannot share a session store.

Sessions

A random ID in a cookie pointing at server state. You get instant revocation, small cookies, and a logout that actually works. Every verifier needs to reach the session store.

JWTs

The token carries its own signed claims, so microservices validate requests without a network hop and tokens can be handed to third parties. A stateless JWT is valid until it expires.

The trade-off is revocation. "Log out everywhere" and "ban this account now" require short lifetimes plus refresh tokens, or a denylist, and a denylist is just a session store you rebuilt with extra steps.

The hybrid most production systems land on: short-lived access JWTs (5-15 minutes) for service-to-service verification, backed by a stateful refresh token you can actually revoke.

How do you store passwords safely?junior

Never encrypted, never plain: hashed with a slow, salted, memory-hard algorithm. Argon2id is the current recommendation, bcrypt remains acceptable. General-purpose hashes like SHA-256 are wrong even with a salt, because GPUs compute billions per second; password hashes are deliberately expensive, tuned so one verification costs 50-100ms on your hardware.

The salt, random and per-password, kills precomputed rainbow tables and makes identical passwords hash differently. Good libraries handle it: bcrypt.hash() output embeds the salt and cost factor, so you store a single string.

The extras that signal seniority:

  • A pepper (a server-side secret outside the database), so a dump alone is not crackable.
  • Rehashing on successful login as hardware improves and cost parameters rise.
  • Rate limiting on the login endpoint, because online guessing bypasses your hashing entirely.
How do you prevent SQL injection and its cousins?mid

Never build queries by string concatenation: use parameterized queries so user input travels as data, never as SQL text.

ts
// vulnerable
db.query(`SELECT * FROM users WHERE email = '${email}'`);
// safe: the value is sent separately from the SQL text
db.query("SELECT * FROM users WHERE email = $1", [email]);

Escaping is the fragile fallback; parameterization removes the bug class. ORMs give it by default, but their raw-SQL escape hatches reopen the hole, which is where real incidents come from.

The cousins share the same root cause, code and data got mixed: command injection, path traversal, and NoSQL injection ({"$gt": ""} arriving where a string was expected) are the same bug in different syntax.

  1. keep input as dataparameterizeSQL parameters; an argument array, never a shell string
  2. validate type and shape at the boundaryresolve paths against a base directory, reject unexpected types
  3. limit the blast radiusleast-privilege database users: a successful injection reads one schema instead of dropping it

Scaling & operations

Horizontal vs vertical scaling: what changes in your code?junior

The interview trap is treating this as an infrastructure question when it is really a state question.

Vertical (a bigger box)

Changes nothing in your code, which is exactly why it is the right first move. Its limits are a price ceiling and a single point of failure.

Horizontal (more boxes)

Breaks every assumption about where state hides: in-memory sessions fail because the next request lands on a different instance, local file uploads vanish, in-process caches drift apart across nodes, and cron jobs or "singleton" loops suddenly run N times.

So the code changes are all about externalizing state: sessions to Redis, files to object storage, caches to a shared tier (or accept bounded per-node staleness), scheduled work behind a distributed lock or a dedicated runner.

You also inherit a load balancer, health checks, and rolling deploys.

Go deeper: System Design track (in development)

Why must services be stateless to scale, and where does the state go?mid

Stateless means any instance can serve any request, which is what lets a load balancer treat instances as interchangeable and an autoscaler add or kill them freely.

The moment an instance holds something a later request needs (a session, a half-processed upload, a WebSocket's conversation context), you need sticky routing. Stickiness breaks autoscaling, rolling deploys, and failover: draining a node logs its users out.

The state does not disappear; it moves one hop away into systems built to hold it:

  • Sessions and hot data to Redis.
  • Files to object storage.
  • Durable records to the database.
  • In-flight work to a queue.

"Stateless" really means "state lives in something replicated that survives this instance". The honest caveat: per-instance caches are fine as an optimization with bounded staleness, never as the source of truth.

How should a healthy service behave when a dependency goes down?senior

It should shed the broken dependency, not mirror its failure. That takes three mechanisms:

  • Aggressive timeouts: a 30-second hang per call is how one slow dependency exhausts your thread and connection pools and takes you down with it.
  • A circuit breaker, so you stop paying the timeout once failure is established.
  • A fallback per feature: serve cached or stale data, hide the recommendations panel, queue the write for later, or return a partial response with the degraded section marked.

The design question to answer per dependency is "what do we serve instead?", and it must be decided before the incident, not during.

Retries need budgets and jitter or they become a self-inflicted DDoS on a recovering dependency. And health checks must distinguish "I am down" from "my dependency is down", or the orchestrator will restart perfectly healthy instances straight into the same outage.

Go deeper: When the Model Has a Bad Day · LLM API Retries, Timeouts, and Fallbacks: A Production Playbook

Keep the answers fresh until the interview

New decks, lessons, and guides ship weekly. One email when they do; reading stays free, no account.