Organize the answer around ownership, limits, failure, and recovery. Definitions become interview-ready when they survive a concrete production scenario.
Question set
26 detailed answers
01What types of NoSQL databases are there, and when do you choose each?
junior
Short answer: Four main types: key-value (Redis, DynamoDB), document (MongoDB, CouchDB), column-family / wide-column (Cassandra, HBase, ScyllaDB), graph (Neo4j, JanusGraph). The choice depends on your data model and access patterns.
In detail:
| Type | Model | When to use | Examples |
|---|---|---|---|
| Key-value | Key → value (blob) | Cache, sessions, simple lookup by key, counters | Redis, Memcached, DynamoDB |
| Document | Key → JSON/BSON document | Flexible schema, nested objects, catalogs, profiles | MongoDB, CouchDB, Firestore |
| Column-family | Row → columns (sparse), partitioned by key | Huge write volumes, time-series, logs, IoT, write-heavy | Cassandra, HBase, ScyllaDB |
| Graph | Nodes + edges (relationships as first-class citizens) | Social networks, recommendations, fraud detection, relationship graphs | Neo4j, ArangoDB, JanusGraph |
How to choose in practice:
- Just need a fast cache / distributed key-value store → key-value (Redis).
- Your data is "documents" with variable fields, and aggregates are read whole → document (MongoDB).
- A giant write stream, you need linear horizontal scalability, no complex joins → column-family (Cassandra).
- The essence of the data is relationships and graph traversal (friends of friends, shortest path) → graph (Neo4j).
// Neo4j — find friends of friends (which in SQL would require repeated JOINs)
MATCH (me:User {name: 'Alice'})-[:FRIEND]->()-[:FRIEND]->(fof)
WHERE NOT (me)-[:FRIEND]->(fof) AND me <> fof
RETURN DISTINCT fof.name
-- Cassandra — the table is designed for a specific query
CREATE TABLE events_by_user (
user_id uuid,
event_time timestamp,
event_type text,
payload text,
PRIMARY KEY (user_id, event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);
⚠️ Gotcha: A graph can be emulated in a relational database, but traversing many "hops" (depth N) kills SQL with recursive joins — that's the classic signal to use a graph database. And vice versa: don't reach for Neo4j where there are few relationships — you lose in simplicity.
02When should you choose SQL and when NoSQL?
middle
Short answer: SQL — when you need a strict schema, complex joins, ACID transactions, and the data is highly interrelated. NoSQL — when you need a flexible/changing schema, huge volume, horizontal scaling, and you understand your access patterns.
In detail:
| Criterion | SQL (relational) | NoSQL |
|---|---|---|
| Schema | Rigid, defined up front (schema-on-write) | Flexible / schemaless (schema-on-read) |
| Scaling | Mostly vertical (a beefier server) | Horizontal (sharding across nodes) |
| Joins | Native, powerful | Usually none (denormalization or app-side join) |
| Transactions | Full ACID | Often limited / eventual |
| Consistency | Strong | Often eventual (BASE) |
| Queries | Declarative SQL, flexible ad-hoc | Optimized for known patterns |
Schema-on-write vs schema-on-read:
- SQL validates the structure on write — the data is always "clean," but migrations are expensive.
- NoSQL accepts any documents — flexible at the start, but the validation/compatibility logic moves into the application.
Scaling:
- Vertical (scale up) — add CPU/RAM to a single server. Simple, but there's a ceiling and it's expensive.
- Horizontal (scale out) — add nodes. NoSQL is designed for this (sharding + replication). In SQL, sharding is possible but painful (you lose joins and cross-shard transactions).
⚠️ Gotcha: "NoSQL = no schema" is a myth. There's always a schema, it's just implicit and lives in the application code. Ignoring this leads to a "garbage dump" of incompatible document versions that's impossible to query.
03When should you NOT reach for NoSQL?
concept
Short answer: When the data is highly interrelated, you need transactions and complex ad-hoc queries/analytics, and the volume fits on one or two servers. In other words — in most typical business applications.
In detail: Don't reach for NoSQL if:
- You need transactions across multiple entities (money, orders, inventory) — the classic ACID of relational databases is more reliable here.
- The data is relational by nature — lots of many-to-many relationships, joins needed.
- The queries aren't known in advance — analytics, BI, ad-hoc reports. SQL is more flexible; in NoSQL you pay for every unplanned pattern with denormalization.
- The data volume is modest — Postgres easily handles terabytes and millions of rows. Chasing "web scale" prematurely is over-engineering.
- The team knows SQL — the operational maturity and tooling of relational databases are vast.
Modern Postgres can also do JSONB (documents), arrays, full-text search — it often covers the "NoSQL needs" without a separate database.
⚠️ Gotcha: Choosing NoSQL "because it's trendy" or "in case we grow to Google scale" is the most common mistake. First prove that a relational database can't cope, and only then switch.
04Explain the CAP theorem. Why can't you have it all at once?
senior
Short answer: In a distributed system, of the three properties — Consistency, Availability, Partition tolerance — when a network partition (P) occurs, you can guarantee only one of two: either consistency (CP) or availability (AP).
In detail:
- C (Consistency) — every read sees the latest write (or an error). All nodes are in agreement.
- A (Availability) — every request gets a (non-error) response, with no guarantee that it's the freshest.
- P (Partition tolerance) — the system keeps working when messages between nodes are lost/delayed.
Why "pick 2 of 3" is a simplification. In a real distributed system the network can always partition, so P is mandatory. The real choice is between C and A at the moment of a partition:
- CP systems — during a partition, sacrifice availability for consistency (refuse writes on the "cut off" part). Examples: MongoDB (with majority writes), HBase, Redis (in strict consistency mode), etcd/ZooKeeper, relational databases with synchronous replication.
- AP systems — during a partition, stay available but may return stale data (converging later). Examples: Cassandra, DynamoDB, CouchDB, Riak.
A partition happened
|
+-------+-------+
| |
CP: I refuse AP: I respond,
to respond, but possibly
but the data with stale
is consistent data
💡 Important: When there's no partition, you can have both C and A simultaneously. CAP describes behavior specifically during a network failure. A more precise model is PACELC: if there's a Partition — choose A/C; Else (in normal operation) — choose Latency/Consistency.
⚠️ Gotcha: Calling something a "CA system" (Consistency + Availability without P) for a distributed database is almost always wrong. CA is a single server. Any system with a network between nodes must be P-tolerant.
05What's the difference between ACID and BASE?
middle
Short answer: ACID — strict transaction guarantees (classic SQL), focused on consistency. BASE — soft guarantees (NoSQL), focused on availability and scalability, with consistency achieved over time.
In detail:
ACID:
- Atomicity — the transaction happens entirely or not at all.
- Consistency — a transition from one valid state to another (respecting constraints).
- Isolation — concurrent transactions don't interfere with each other.
- Durability — what's committed survives a crash.
BASE:
- Basically Available — the system always responds (possibly partially/stale).
- Soft state — the state may change over time without new writes (due to replication).
- Eventual consistency — over time all replicas converge.
BASE is essentially the philosophy of AP systems: sacrifice strict consistency for availability and horizontal scale.
⚠️ Gotcha: "NoSQL can't do ACID" is an outdated myth. MongoDB since 4.0 supports multi-document ACID transactions, Redis has MULTI/EXEC. But these capabilities are often limited (performance, scope), and you shouldn't abuse them in a distributed setting.
06What are eventual consistency and strong consistency?
middle
Short answer: Strong consistency — after a write, any read immediately sees the new value. Eventual consistency — replicas synchronize with a delay, so a read may temporarily return stale data, but "eventually" everyone sees the current value.
In detail:
- Strong consistency — a linear history, as if there's a single copy of the data. The cost: higher latency, lower availability during failures (you need acknowledgment from a quorum/all replicas).
- Eventual consistency — a write is acknowledged quickly and propagates asynchronously. The inconsistency window is usually milliseconds to seconds. The cost: the application must be able to live with stale data.
Between them there are intermediate models: read-your-writes, monotonic reads, causal consistency.
Example of eventual consistency: you liked something, but a friend on another continent still sees the old counter for another second — that's fine for a social network, unacceptable for a bank balance.
Many systems are configurable: Cassandra/DynamoDB let you set the consistency level per request (ONE, QUORUM, ALL). The quorum rule: if R + W > N (read + write replicas > total replicas), you get strong consistency.
⚠️ Gotcha: Eventual consistency is treacherous in read-after-write scenarios: a user saved their profile, immediately opened it, and saw the old version — a bug from a UX standpoint. The solution is to read from the primary/leader or use read-your-writes.
07What is Redis?
junior
Short answer: Redis (REmote DIctionary Server) is an in-memory key-value data store, used as a cache, a database, a message broker, and a queue. It keeps data in RAM, which makes it very fast (sub-millisecond latency).
In detail:
- In-memory — all data is in RAM, hence the speed. There's optional persistence to disk.
- Not just strings — it supports a rich set of data structures (see below).
- Single-threaded for commands (event loop), which simplifies the model and removes locking.
- Atomic operations, TTL on keys, pub/sub, Lua scripts, transactions.
redis-cli SET user:1:name "Alice"
redis-cli GET user:1:name # "Alice"
redis-cli SET session:abc "data" EX 3600 # with a 1-hour TTL
redis-cli TTL session:abc # 3600
⚠️ Gotcha: By default Redis keeps the entire dataset in RAM. It's not "infinite" storage — you need to watch maxmemory and the eviction policy, otherwise OOM.
08Why is Redis single-threaded yet so fast?
concept
Short answer: Because the bottleneck isn't the CPU, it's memory and the network. Single-threading removes the overhead of locking, context switching, and race conditions, while data in RAM + an efficient event loop (epoll/kqueue) deliver sub-ms speed.
In detail: Reasons for the speed with a single thread:
- Data in RAM — no disk I/O during operations (memory access is orders of magnitude faster than disk).
- No locks or contention — one thread = no mutexes, no cache-line bouncing between cores, no deadlocks.
- I/O multiplexing — epoll/kqueue handles thousands of connections in a single loop without a thread per connection.
- Efficient data structures — optimized implementations (skip lists for ZSet, special encodings for small collections).
- Simple model — each command is atomic "for free," with no complex synchronization.
Nuances:
- "Single-threaded" refers to command execution. As of Redis 6+ there's threaded I/O (reading/writing sockets across multiple threads), and background tasks (persistence, deleting large keys via
UNLINK) already run in separate threads. - To use all cores you run multiple Redis instances (or Cluster).
⚠️ Gotcha: One "heavy" command blocks the entire server. KEYS * on a large database, SMEMBERS on a huge Set, or sorting a large list will halt processing for all other clients. Use SCAN, and avoid O(N) commands on large structures.
09What data structures does Redis have, and what is each for?
middle
Short answer: String, List, Hash, Set, Sorted Set (ZSet), Stream, plus "probabilistic"/special ones — HyperLogLog, Bitmap, Geo. Each covers its own class of tasks.
In detail:
String — the simplest type (text, number, binary up to 512 MB). Cache, counters, flags.
SET counter 0
INCR counter # atomic increment → 1
INCRBY counter 10 # → 11
APPEND log "line\n"
List — a linked list of strings. Queues, stack, the last N elements.
LPUSH queue "job1" # add on the left
RPUSH queue "job2" # add on the right
RPOP queue # take from the right (FIFO with LPUSH)
LRANGE queue 0 -1 # all elements
BLPOP queue 5 # blocking pop (for workers)
Hash — a field→value dictionary within a key. Objects/entities.
HSET user:1 name "Alice" age 30
HGET user:1 name # "Alice"
HGETALL user:1
HINCRBY user:1 age 1 # → 31
Set — an unordered collection of unique strings. Tags, unique visitors, set operations.
SADD tags:post:1 "redis" "nosql"
SISMEMBER tags:post:1 "redis" # 1
SINTER tags:post:1 tags:post:2 # intersection
SCARD tags:post:1 # size
Sorted Set (ZSet) — a set with a numeric score, sorted. Leaderboards, priority queues, time-series, rate limiting.
ZADD leaderboard 100 "player1" 250 "player2"
ZINCRBY leaderboard 50 "player1" # → 150
ZREVRANGE leaderboard 0 9 WITHSCORES # top 10
ZRANK leaderboard "player1" # the player's rank
Stream — an append-only log of records (like Kafka-lite). Event sourcing, queues with consumer groups, reliable delivery.
XADD events * type "click" user "1" # * = auto-ID
XREAD COUNT 10 STREAMS events 0
XGROUP CREATE events workers $
XREADGROUP GROUP workers w1 COUNT 1 STREAMS events >
HyperLogLog — probabilistic counting of unique elements with ~0.81% error in ~12 KB. Unique visitors per day across millions of values.
PFADD visitors "user1" "user2" "user3"
PFCOUNT visitors # approximate number of uniques
Bitmap — bits on a key. Activity flags (was a user online on day X), compact sets by id.
SETBIT active:2026-06-24 1001 1 # user 1001 was active
GETBIT active:2026-06-24 1001 # 1
BITCOUNT active:2026-06-24 # how many were active
⚠️ Gotcha: Storing large JSON in a String instead of a Hash means losing partial updates (you'll have to read/parse/write the whole object). And conversely, a great many small keys instead of a Hash bloat memory with key overhead.
10What are the typical use cases for Redis?
middle
Short answer: Cache, session storage, rate limiting, leaderboards (ZSet), task queues (List/Stream), pub/sub, distributed locks.
In detail:
1. Cache — the most common. We store query/computation results with a TTL.
SET cache:user:1 "{...json...}" EX 300
2. Sessions — server-side sessions in a distributed application (multiple instances share one store).
SETEX session:token123 1800 "{userId: 1, role: admin}"
3. Rate limiting — limiting request frequency.
# Fixed window: a counter per window
INCR rate:user:1:minute
EXPIRE rate:user:1:minute 60 # if >limit — we block
For a sliding window, a ZSet with timestamps is used.
4. Leaderboard — real-time rankings via a ZSet (see ZADD/ZREVRANGE above).
5. Queues — LPUSH + BRPOP for simple queues, Streams + consumer groups for reliable ones.
6. Pub/Sub — broadcasting messages to subscribers (chats, notifications, cache invalidation).
SUBSCRIBE news # subscriber
PUBLISH news "hello" # publisher
⚠️ Pub/Sub in Redis is fire-and-forget: if no subscriber is present at the moment of publication, the message is lost. If you need delivery guarantees — Streams.
7. Distributed locks — SET key token NX PX ttl creates a time-bounded lease. Release it with a Lua script only if the token still matches; when a stale owner could corrupt data, the protected resource also needs monotonically increasing fencing tokens.
⚠️ Gotcha: Using Redis Pub/Sub as a full-fledged message queue is a mistake. There's no persistence, no acknowledgments, no retries. For queues — use a List or Streams.
11How do you make a distributed lock in Redis? What is Redlock?
senior
Short answer: A simple lock — SET key value NX PX ttl (atomically: set if it doesn't exist, with a TTL). Release — via a Lua script that checks the owner. Redlock is an algorithm for locking across several independent Redis nodes for increased reliability.
In detail:
Simple lock (one Redis):
# Acquire: only if the key doesn't exist, with a unique token and TTL
SET lock:resource <random-token> NX PX 30000
-- Release: delete only if we're the owner (atomically)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
NX— don't overwrite someone else's lock.PX ttl— auto-release, so there's no eternal lock if the client crashes.- A unique token — so you don't release a lock that another client grabbed after the TTL expired.
Redlock (multiple nodes): Antirez's algorithm for N independent masters (with no replication between them):
- Get the current time.
- Sequentially try to acquire the lock on all N nodes with the same token and a small timeout.
- The lock is considered acquired if it was obtained on a majority (N/2+1) of nodes AND the total time < TTL.
- The effective TTL = the original TTL minus the time spent.
- If it failed — release all nodes.
⚠️ Gotcha: Redlock is controversial (Martin Kleppmann's critique): because of GC pauses, clock skew, and network delays, two clients may simultaneously believe they own the lock. For correctness (not just optimization) you need a fencing token — a monotonic counter that the protected resource checks. Redis locks are good for efficiency, but not as the sole guarantee of mutual exclusion in critical systems.
12How does Redis persist data to disk? RDB vs AOF.
middle
Short answer: Two mechanisms: RDB — periodic snapshots of the entire dataset (compact, fast restart, but you lose data between snapshots). AOF — a log of all write commands (higher durability, but a larger file and slower restart). You can use both.
In detail:
RDB (Redis Database):
- A binary point-in-time snapshot (
SAVE/BGSAVE, on a schedulesave 900 1). BGSAVEforks the process — the snapshot is made in the background (copy-on-write).- + Compact, fast to load on restart, minimal performance impact.
- − On a crash you lose data since the last snapshot (minutes).
AOF (Append Only File):
- Logs every command that modifies data.
fsyncpolicies:always(every write, the most durable, slow),everysec(once per second — a compromise, loss ≤1 sec),no(at the OS's discretion).- A rewrite is performed periodically — compacting the log.
- + Higher durability, the log is human-readable.
- − A larger file than RDB, slower restart, higher disk load.
What gets lost:
- RDB only → data since the last snapshot (for example, up to 15 minutes).
- AOF
everysec→ up to ~1 second. - AOF
always→ practically nothing, but slow.
Recommendation: enable both — AOF for durability + RDB for fast backups/restart. Modern Redis (7+) can do multi-part AOF (RDB preamble + an incremental log).
⚠️ Gotcha: Treating Redis as reliable as a primary database "out of the box" is dangerous. Even with AOF everysec you can lose a second of data, and with always you take a performance hit. For critical data, Redis is a cache/accelerator, and the source of truth is a durable database.
13What are eviction policies in Redis and how does maxmemory work?
middle
Short answer: maxmemory sets the RAM limit. When it's reached, Redis applies an eviction policy — which keys to remove: by LRU, LFU, TTL, randomly, or not remove at all (return an error).
In detail:
The maxmemory 2gb parameter + maxmemory-policy <policy>. Policies:
| Policy | What it does |
|---|---|
noeviction |
Doesn't remove; new writes → error (default). Good for "Redis as a database" |
allkeys-lru |
Evicts the least recently used among all keys. The classic for a cache |
volatile-lru |
LRU only among keys with a TTL |
allkeys-lfu |
The least frequently used among all (Least Frequently Used) |
volatile-lfu |
LFU among keys with a TTL |
allkeys-random |
Random among all |
volatile-random |
Random among keys with a TTL |
volatile-ttl |
Keys with the nearest TTL expiry |
LRU vs LFU:
- LRU — throws out the ones not used in a while. Vulnerable to "bursts" (a single rare scan will evict hot data).
- LFU (Redis 4+) — accounts for usage frequency, better at keeping genuinely hot keys. Usually preferable for a cache.
Redis uses an approximate LRU/LFU (it samples several keys, doesn't scan them all) — a trade-off of accuracy/speed, tuned via maxmemory-samples.
⚠️ Gotcha: The default policy is noeviction. If you use Redis as a cache and haven't changed it, then when memory fills up, writes will start failing with an OOM command not allowed error rather than "self-cleaning." For a cache, explicitly set allkeys-lru/allkeys-lfu.
14What's the difference between replication, Sentinel, and Cluster in Redis?
senior
Short answer: Replication — copies master→replica (read scaling, backup). Sentinel — a monitoring and automatic failover system (HA for a single shard). Cluster — horizontal sharding of data across nodes + built-in HA.
In detail:
Replication (master-replica):
- One master accepts writes; replicas copy the data asynchronously.
- Replicas serve reads → read scaling.
- Asynchrony → possible loss of writes if the master crashes before replication (eventual consistency).
- On its own it does not do automatic failover.
Sentinel:
- A set of watcher processes that monitor the master and replicas.
- When the master fails, by a quorum of sentinels they elect a new replica as master (automatic failover).
- Clients learn the new master's address via Sentinel.
- Suitable for HA, but data isn't sharded — the entire dataset must fit on one node.
Cluster:
- Data is sharded across 16384 hash slots distributed among master nodes.
- Key → CRC16(key) mod 16384 → slot → node.
- Each master has its own replicas (built-in HA + failover).
- Horizontal scaling of both writes and data volume.
- Limitations: multi-key operations work only if the keys are in the same slot (using hash tags
{user1}:profile,{user1}:settings).
Standalone + Replication: [master] → [replica] [replica] (read scaling)
Sentinel: [sentinel x3] watch, failover (HA, 1 shard)
Cluster: [m1+r][m2+r][m3+r], 16384 slots (sharding + HA)
⚠️ Gotcha: In Cluster you can't just run operations over multiple keys (MGET, transactions, Lua with different keys) if they're in different slots — you'll get a CROSSSLOT error. You need to design keys with hash tags so that related data lives in the same slot.
15What caching patterns are there? Cache-aside, read/write-through, write-behind.
middle
Short answer: Cache-aside (lazy loading) — the application manages the cache itself. Read-through/write-through — the cache reads/writes to the database synchronously on its own. Write-behind (write-back) — the cache writes to the database asynchronously.
In detail:
Cache-aside (lazy loading) — the most widespread:
def get_user(user_id):
data = redis.get(f"user:{user_id}")
if data is None: # cache miss
data = db.query_user(user_id) # read from the database
redis.set(f"user:{user_id}", data, ex=300) # put it in the cache
return data
- The application is responsible for loading and invalidation.
- Only what's requested makes it into the cache (lazy).
- − The first request is always a miss; cache and database can become inconsistent.
Read-through — the cache itself loads data from the database on a miss (the logic is in the cache layer/library). The application always talks only to the cache.
Write-through — on a write, the application writes to the cache, and the cache synchronously writes to the database.
- + The cache is always consistent with the database.
- − Writes are slower (two operations); rarely-read data gets cached too.
Write-behind (write-back) — write to the cache, and to the database asynchronously (in batches/with a delay).
- + Very fast writes, you can aggregate.
- − Risk of data loss if the cache crashes before flushing; more complex.
| Pattern | Read | Write | Risk |
|---|---|---|---|
| Cache-aside | App ↔ Cache ↔ DB | App → DB (+ invalidation) | Stale data, miss penalty |
| Read-through | App ↔ Cache → DB | — | — |
| Write-through | — | App → Cache → DB (sync) | Slower writes |
| Write-behind | — | App → Cache → DB (async) | Data loss |
⚠️ Gotcha: In cache-aside, a common mistake on write is to update the cache instead of deleting it. Under a race (two concurrent updates), the cache may stay with the old value forever. It's safer to invalidate (delete) the key on a database write rather than overwrite it.
16What's so hard about cache invalidation? ("one of the two hard things in CS")
concept
Short answer: The difficulty is removing/updating stale data promptly and accurately everywhere it's cached, without hitting races, leaving stale data behind, or crashing the database with a flood of misses. It's fundamentally about consistency in a distributed system.
In detail: The well-known quote by Phil Karlton: "There are only two hard things in Computer Science: cache invalidation and naming things."
Why it's hard:
- Race conditions: between reading from the database, writing to the cache, and other threads updating the database, it's easy to cache a stale value. A classic problem: a read miss loads the old value into the cache right after another thread has already updated the database and invalidated the cache.
- Multiple copies: the same data may sit in the application cache, Redis, a CDN, the browser — you need to invalidate everywhere.
- Dependencies: changing one entity may affect many derived caches (aggregates, lists, denormalized views). It's hard to track "what went stale."
- TTL balance: a short TTL → many misses and load on the database; a long one → stale data for longer.
Invalidation strategies:
- TTL (expiration) — the simplest: a key lives N seconds. "Eventual consistency" for the cache. Simple, but allows temporary staleness.
- Explicit invalidation — delete the key on a write (delete-on-write). More precise, but harder (you need to know all the keys).
- Write-through — the cache is always in sync, but slower.
- Versioning / key namespacing — change part of the key when something changes (
user:1:v2); the old ones get evicted on their own. - Event-based — invalidation via events/pub-sub (for example, CDC from the database).
⚠️ Gotcha: "I'll just set a TTL" works until the business demands "see changes immediately." And explicit invalidation is easy to forget on some write path (especially with batches, migrations, direct updates in the database). The best default is TTL + explicit invalidation on hot paths.
17What is cache hit ratio and why does it matter?
junior
Short answer: Hit ratio = the share of requests served from the cache out of the total. hit_ratio = hits / (hits + misses). The higher it is, the more effectively the cache offloads the data source.
In detail:
- Hit — the data was found in the cache (fast, no database access).
- Miss — it's not in the cache, so we go to the database (slow) and (usually) put it in the cache.
- A high hit ratio (for example, 90%+) means the database receives only 10% of requests.
In Redis, you check the metrics via INFO stats:
redis-cli INFO stats | grep keyspace
# keyspace_hits:100000
# keyspace_misses:5000
# hit_ratio = 100000 / 105000 ≈ 95.2%
A low hit ratio signals: too short a TTL, the wrong keys, too small a cache (frequent evictions), or poor access locality (the data is rarely repeated).
⚠️ Gotcha: Chasing a hit ratio of 100% is pointless: for cold/rare data the cache is useless and only wastes memory. It's more important to cache "hot" data (Pareto: 20% of the data accounts for 80% of requests). Also, a high hit ratio on stale data is a "good metric, bad outcome."
18What is a cache stampede / thundering herd and how do you deal with it?
senior
Short answer: A cache stampede (also known as thundering herd, dogpile) is when a popular key expires and a large number of requests miss simultaneously and all hit the database at once, overloading it. You fight it with locking, early recomputation, and background refresh.
In depth: Scenario: a hot key with a TTL expires. At that moment a thousand parallel requests all see a miss → they all go to the database to recompute the same thing → a load spike, possibly bringing the database down.
Mitigation techniques:
- Locking / mutex (single-flight): the first request that misses takes a lock and recomputes, while the rest wait for the result or serve the stale value.
def get(key):
val = redis.get(key)
if val: return val
if redis.set(f"lock:{key}", 1, nx=True, ex=10): # only one recomputes
val = recompute()
redis.set(key, val, ex=300)
redis.delete(f"lock:{key}")
return val
else:
time.sleep(0.05) # wait and re-read
return get(key)
- Probabilistic early expiration — refresh the key before it expires, with a probability that grows as the TTL approaches (the XFetch algorithm). Spreads out the recomputation.
- Background refresh — a background worker refreshes hot keys on a schedule, so the cache "never goes empty."
- Stale-while-revalidate — serve the stale value while the refresh runs in the background.
⚠️ Gotcha: Using the same TTL for a batch of related keys leads to synchronized expiration and a stampede. Add jitter (a random spread in the TTL, e.g. 300 ± 30 sec).
19What are cache penetration, cache avalanche, and a hot key?
senior
Short answer: Penetration — requests for nonexistent data pass straight through the cache to the database. Avalanche — mass simultaneous expiration/failure of the cache crashes the database. Hot key — a single key receives a disproportionate amount of traffic, overloading a node.
In depth:
Cache penetration: Requests for data that exists neither in the cache nor in the database (e.g. a nonexistent id, or a brute-force attack). Every time it's a miss → a hit on the database, and the cache doesn't help.
- Solution 1: cache the "empty" result (null/sentinel) with a short TTL.
- Solution 2: Bloom filter — check the key's existence before going to the database.
SET cache:user:99999 "__NULL__" EX 60 # negative caching
Cache avalanche: A large number of keys expire simultaneously, OR the cache server goes down → all the traffic hits the database at once → cascading failure.
- Solution 1: jitter in TTLs (spread out the expiration times).
- Solution 2: multi-level cache (local L1 + L2 Redis).
- Solution 3: circuit breaker / rate limiting toward the database, graceful degradation.
- Solution 4: an HA Redis cluster, so that one node going down doesn't wipe out the whole cache.
Hot key: A single key (e.g. a product on sale, a celebrity) collects a huge share of requests → one node/cluster slot is overloaded.
- Solution 1: an application-side local cache for the top keys.
- Solution 2: replicate the key with suffixes (
hot:1#1,hot:1#2) and read a random replica. - Solution 3: read replicas.
| Problem | Essence | Cure |
|---|---|---|
| Penetration | Request for nonexistent data | Null cache, Bloom filter |
| Avalanche | Mass simultaneous expiration/failure | TTL jitter, HA, circuit breaker |
| Stampede | Race to recompute a single hot key | Lock, background refresh |
| Hot key | Traffic skew toward a single key | Local cache, key replication |
⚠️ Gotcha: Penetration and avalanche are often confused. Penetration is about nonexistent data (the cache is essentially useless), while avalanche is about mass expiration/failure (the cache was there but "collapsed"). They are cured differently.
20Where can you cache data? What cache levels are there?
middle
Short answer: Along the entire request path: client browser → CDN → reverse proxy / API gateway → application cache (in-process / Redis) → database cache (query/buffer cache). The closer to the user, the faster and cheaper, but the harder to invalidate.
In depth:
User
│
▼
[1] Browser (HTTP cache, localStorage) — fastest, on the client
│
▼
[2] CDN (CloudFront, Cloudflare) — static assets, edge, geographically close
│
▼
[3] Reverse proxy / API gateway (Nginx, Varnish) — response cache
│
▼
[4] Application cache:
- In-process (local memory: Caffeine, LRU map) — L1, nanoseconds
- Distributed (Redis, Memcached) — L2, shared
│
▼
[5] Database (buffer pool, query cache) — the database's built-in caching
│
▼
Disk
Trade-off: the higher the level (the closer to the user), the lower the latency and load on the backend, but the harder the invalidation (how do you refresh the cache in the browsers of thousands of users? — only via TTL/URL versioning).
HTTP caching (levels 1-3) is controlled by headers: Cache-Control, ETag, Last-Modified, max-age.
Multi-level application cache (L1+L2): a local in-process cache for the hottest data (minimal network) + Redis as a shared L2. Protects against avalanche and hot keys.
⚠️ Gotcha: Caching at multiple levels multiplies the invalidation problem. You changed the data — but it's cached in Redis, in the local memory of 10 instances, and in the CDN. You need a well-thought-out strategy (events/versions), otherwise users see inconsistent data from different levels.
21What is MongoDB? Documents, collections, BSON.
middle
Short answer: MongoDB is a document-oriented NoSQL database. Data is stored as documents (JSON-like) in BSON format; documents are grouped into collections (analogous to tables), and collections into databases. The schema is flexible.
In depth:
- Document — a set of key-value pairs, which can contain nested objects and arrays. Analogous to a row, but richer.
- Collection — a group of documents. Documents in the same collection can have different fields (schemaless).
- BSON (Binary JSON) — a binary storage format: more compact and faster than JSON, supporting additional types (
ObjectId,Date,Decimal128, binary).
// A document in the users collection
{
"_id": ObjectId("..."),
"name": "Alice",
"age": 30,
"addresses": [ // nested array
{ "city": "Moscow", "zip": "101000" }
],
"tags": ["admin", "premium"]
}
db.users.insertOne({ name: "Bob", age: 25 })
db.users.find({ age: { $gt: 20 } })
When MongoDB is a good fit:
- Data is naturally "document-like" (profiles, product catalogs, CMS content).
- A changing/flexible schema, fast iteration.
- Aggregates are read/written as a whole (a document = the unit of access).
- You need horizontal scaling with sharding.
⚠️ Gotcha: Each document is limited to 16 MB. Unbounded growth of a nested array (e.g. all comments inside a post document) will sooner or later hit the limit and kill performance. That's a signal to move it out into a separate collection.
22How do indexes and aggregations (aggregation pipeline) work in MongoDB?
middle
Short answer: Indexes (B-tree, as in SQL) speed up lookups/sorting; without them you get a full collection scan. The aggregation pipeline is a chain of stages ($match, $group, $sort, $lookup...) for transforming and aggregating data, analogous to GROUP BY/JOIN in SQL.
In depth:
Indexes:
db.users.createIndex({ email: 1 }, { unique: true }) // single-field, unique
db.users.createIndex({ age: 1, name: -1 }) // compound
db.posts.createIndex({ title: "text" }) // text
db.places.createIndex({ location: "2dsphere" }) // geo
Without an index, a query does a COLLSCAN (full scan). explain() shows the plan.
Aggregation pipeline — a sequence of stages; data "flows" through them:
db.orders.aggregate([
{ $match: { status: "completed" } }, // filter (like WHERE)
{ $group: { // grouping (like GROUP BY)
_id: "$customerId",
total: { $sum: "$amount" },
count: { $sum: 1 }
}},
{ $sort: { total: -1 } }, // sorting
{ $limit: 10 }
])
⚠️ Gotcha: The order of stages matters for performance. $match and $limit should come as early as possible in the pipeline, to shrink the volume of data before the heavy stages ($group, $lookup). A $match at the start uses indexes; in the middle it no longer does.
23How does MongoDB scale? Replica set, sharding. Is there a join?
senior
Short answer: A replica set is a set of replicas (one primary for writes + secondaries) for HA and read scaling. Sharding is horizontal partitioning of data by a shard key across multiple nodes for volume/write scaling. A "join" is emulated via $lookup.
In depth:
Replica set:
- One primary (accepts writes) + several secondaries (replicate, serve reads).
- When the primary goes down, a new one is automatically chosen (election).
writeConcern(majority) andreadConcerncontrol consistency/durability.- This is about availability and reliability, not volume.
Sharding:
- Data is split by a shard key into chunks, distributed across shards.
mongos(the router) directs queries; config servers store the metadata.- This is about horizontal scaling of volume and writes.
- The choice of shard key is critical: a bad key → skew (jumbo chunks, a hot shard).
Join — $lookup:
MongoDB historically had no join (encouraging denormalization). As of version 3.2, there is $lookup in aggregation:
db.orders.aggregate([
{ $lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user"
}}
])
But $lookup is more expensive than a SQL join (there's no real join optimizer, it works like a nested loop) — it shouldn't be overused.
⚠️ Gotcha: A shard key cannot be easily changed after sharding (although in newer versions this has become possible via resharding — an expensive operation). A poor shard key (e.g. a monotonically increasing _id) leads to all writes going to a single shard. Choose a key with good cardinality and even distribution.
24What is denormalization in NoSQL and query-driven design?
senior
Short answer: Denormalization is the deliberate duplication of data (embedding/copying) so that you can get everything needed for a query in a single read without a join. Query-driven design is designing the data model around query patterns rather than around the structure of entities (as in SQL normalization).
In depth:
In relational databases we normalize (3NF): each fact is stored once, relationships go through FKs, and data is assembled via joins. In NoSQL (especially Cassandra, MongoDB) the approach is the opposite:
- First we determine the queries the application needs.
- We design the tables/documents around those queries so that each one is served by a single read.
- We duplicate data wherever it speeds up reads.
Example (MongoDB): instead of normalized posts + comments + users collections with joins — we embed data that is frequently read together:
// A denormalized post document: everything needed for display in one read
{
"_id": ObjectId("..."),
"title": "Hello",
"author": { "id": 1, "name": "Alice" }, // duplicated the author's name
"comments": [ // embedded the latest comments
{ "user": "Bob", "text": "Nice!" }
]
}
Trade-off:
- + Fast reads (a single request), scales excellently (no cross-node joins).
- − Data duplication (more space). When the source changes (the author's name), you have to update it in all copies → write complexity and the risk of inconsistency.
In Cassandra this is taken to the extreme: "one table per query." The same fact is stored in several tables with different partition keys.
⚠️ Gotcha: Denormalization shifts complexity from reads to writes. If the data changes often and is duplicated across hundreds of documents, updating becomes a nightmare. Denormalize what is frequently read and rarely changed. And don't try to normalize NoSQL like a relational database — you'll lose both performance and the whole point of choosing NoSQL.
25Why have a cache at all, if a database with indexes is already fast?
concept
Short answer: Because even a fast database is orders of magnitude slower than a RAM cache, has limited throughput, and costs more per request. A cache reduces latency, offloads the database, survives traffic spikes, and saves on expensive computations.
In depth: Reasons to cache, even with an indexed database:
- Latency: an indexed query to Postgres is single-digit to tens of milliseconds (disk, network, SQL parsing, the planner). Redis from RAM is sub-millisecond. A difference of 10-100x.
- Throughput / load: the database is a shared resource with a limit on connections and IOPS. The cache takes 90%+ of the reads, leaving the database as a resource for writes and complex queries.
- Computation cost: sometimes the "data" is the result of a heavy aggregation/join/external API call. An index won't help — you need to cache the result.
- Protection against spikes: the cache smooths out spike traffic (a viral post), keeping it from crashing the database.
- Read scaling: scaling Redis is cheaper and simpler than scaling the database.
⚠️ Gotcha: A cache is not a free speedup. It adds a consistency problem (stale data), a new component to operate (another point of failure), and invalidation complexity. If the database copes and the latency is acceptable — a cache may be a premature optimization. Measure first.
26If the cache is a separate component, doesn't it make the system less reliable?
concept
Short answer: Yes, it adds a point of failure, but with proper design (graceful degradation, the cache as an accelerator rather than the source of truth) a cache failure leads to a slowdown, not a system outage.
In depth: The key principle: the cache must not be critical for correctness. If Redis goes down:
- The application must be able to read directly from the database (cache-aside naturally allows this).
- The danger: when the cache fails, all the traffic floods the database (cache avalanche) → you need a circuit breaker, rate limiting, a multi-level cache.
- For critical data (balance, orders) the source of truth is the durable database, and the cache merely speeds things up.
When the cache does become critical (e.g. it stores sessions or is used as the primary store), it must be HA: Redis Sentinel/Cluster, persistence, replication.
⚠️ Gotcha: "The cache is temporary anyway, reliability doesn't matter" is a dangerous stance if sessions hang off the cache (its failure will log everyone out) or if the database can't handle the traffic without the cache. Assess: what would happen if the cache went down right now?
Source notes
References and review policy
RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.