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
A recorded source review is available for 26 of 26 answers. Each reviewed answer lists its date and scope.
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: graph traversal is possible in SQL, including recursive CTEs. A graph database is a candidate when measured traversal workloads and its query model justify the extra system; depth alone does not prove SQL is unsuitable. Wide-column scalability also depends on bounded, well-distributed partitions.
The event example assumes timestamps are unique within a user. For real streams add an event_id clustering component or use an appropriate timeuuid: identical primary keys overwrite the same Cassandra row.
02When should you choose SQL and when NoSQL?
middle
Short answer: Choose an engine from access patterns, invariants and operational needs. SQL versus NoSQL is not a choice between correctness and speed.
| Requirement | What to evaluate |
|---|---|
| Related entities, constraints, changing analytical queries | Relational schema, joins, indexes and transactions; PostgreSQL is a common starting point |
| Data usually read as one bounded nested object | Document model; MongoDB supports indexes, schema validation and multi-document transactions |
| Predictable key lookups at distributed scale | Partition-key design, hot keys, supported consistency and transaction boundaries |
| Relationship traversal dominates | Graph model, query language, measured traversal costs |
For orders, items and payments, first express invariants such as unique payment identity and non-negative stock. For a catalog, compare documents with PostgreSQL JSONB rather than assuming only NoSQL can store flexible data.
⚠️ Gotcha: NoSQL does not mean no schema, no transactions or automatic horizontal scaling. SQL engines can also distribute data. Compare the actual product and configuration, including replication, backups, migrations and team experience.
03When should you NOT reach for NoSQL?
concept
Short answer: Do not add a NoSQL database merely because traffic may grow. Prefer an existing relational system when its constraints, joins, transactions and tools already fit the workload.
Warning signs are unclear queries, many evolving cross-entity reports, costly duplication of frequently changing facts, a poor partition key, or a team that cannot operate another data system. First measure whether PostgreSQL indexing, query changes, pooling or partitioning solve the actual limit.
This is not a claim that NoSQL transactions are unreliable: MongoDB and DynamoDB offer transactional features with documented boundaries and costs. If a document or key-value engine meets the invariants and access patterns better, it can be the right choice.
⚠️ Gotcha: moving complexity into application-side joins and synchronization can outweigh a simpler storage model. Make that cost explicit before migrating.
04Explain the CAP theorem. Why can't you have it all at once?
senior
Short answer: During a network partition, a replicated service cannot guarantee both linearizable operations and successful responses to every request reaching a non-failed node.
- C (consistency): operations behave like one up-to-date copy and respect real-time order: a read begun after a completed write sees that write or a later one.
- A (availability): every request reaching a non-failed node eventually completes successfully; returning an error is not CAP availability.
- P (partition tolerance): the model permits messages between groups of nodes to be lost or delayed. You cannot remove this possibility by choosing two letters.
Imagine two isolated replicas and an acknowledged update on one side. The other side must either refuse/wait for a safe answer or answer without knowing the update. That is the C/A tradeoff during the partition.
⚠️ Gotcha: CAP is not a permanent product label or an uptime percentage. A system may offer different operations and read modes. MongoDB majority writes alone do not make every read linearizable; Redis has no universal switch that turns it into a consensus-based CP store.
05What's the difference between ACID and BASE?
middle
Short answer: ACID describes transaction guarantees; BASE is an informal description of systems that allow temporary inconsistency and converge. They are not mutually exclusive labels for SQL and NoSQL.
- Atomicity: the transaction commits as a unit or does not commit.
- Consistency: committed changes preserve the declared rules, assuming the application and constraints express them correctly.
- Isolation: concurrent transactions interact according to the chosen isolation level.
- Durability: acknowledged commits survive the failures covered by the configured storage/replication contract.
BASE expands to Basically Available, Soft state, Eventually consistent. It emphasizes availability and asynchronous convergence; it does not prove 100% uptime or automatic conflict resolution. A database can have ACID local transactions and eventually consistent replicas.
⚠️ Gotcha: Redis MULTI/EXEC prevents interleaving of the queued commands, but runtime command errors do not roll back earlier commands. Durability also depends on persistence settings. Calling MULTI/EXEC fully ACID without these qualifications teaches the wrong rollback model.
06What are eventual consistency and strong consistency?
middle
Short answer: Linearizable (strong) operations respect a single real-time order. Eventual consistency allows stale replicas but promises convergence if updates stop and communication/reconciliation eventually succeeds. It gives no fixed delay bound by itself.
For an account display after saving, read-your-writes may be enough: route the session to a suitable primary or wait for the replica to apply a known position. For enforcing a multi-row balance invariant, replication consistency alone is insufficient: use an appropriate atomic transaction and isolation level.
A quorum rule R + W > N gives overlap under its assumptions, not linearizability by itself. Conflict resolution, concurrent writes, failed writes, membership changes and the read protocol also matter.
Examples must name the API: DynamoDB uses ConsistentRead for eligible tables/local secondary indexes, while global secondary index reads are eventually consistent. Cassandra has named consistency levels such as ONE and QUORUM; those names are not DynamoDB settings.
⚠️ Gotcha: a successful majority write does not mean any arbitrary replica is fresh. Select and verify the read guarantee explicitly.
07What is Redis?
junior
Short answer: Redis is a data-structure store designed around in-memory operation. It is used for caching, counters, sessions and queues. Low latency depends on the command, workload and environment; persistence, memory limits and failure behavior must be configured separately.
In detail:
- In-memory — all data is in RAM, hence the speed. There's optional persistence to disk.
- Not just strings — it supports strings, hashes, lists, sets, sorted sets and streams.
- Most commands execute serially on the main thread. Background work and, depending on version/configuration, network I/O can use other threads; Redis is not universally single-threaded.
- 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: Redis commonly executes commands on one main thread. In-memory data, efficient structures and event-driven networking make many operations fast without per-command thread coordination. CPU can still be the bottleneck: a long command delays other clients.
In detail: Reasons for the speed with a single thread:
- Data in RAM — normal lookups avoid reading the dataset from disk; persistence, fsync, swapping or fork can still affect latency.
- 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:
- Command execution is largely serialized; configured I/O threads and background threads help with other work. BGSAVE and AOF rewrite use child processes, so persistence is not simply another execution thread.
- 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 0
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.
A Stream consumer group tracks pending deliveries. Acknowledge completed work with XACK; recover abandoned pending entries with XAUTOCLAIM or XCLAIM according to the retry policy. These mechanics still need idempotent effects and persistence appropriate to the required durability.
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.
-- KEYS[1] = rate:user:1:<UTC-minute-number>, ARGV[1] = limit
local n = redis.call('INCR', KEYS[1])
if n == 1 then redis.call('EXPIRE', KEYS[1], 120) end
return n <= tonumber(ARGV[1]) and 1 or 0
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.
Execute the rate-limit snippet atomically with EVAL/EVALSHA. The caller constructs a key for the current UTC minute; its 120-second TTL is cleanup, not a sliding extension on every request. Fixed windows permit a burst at the boundary. Simple list pops remove jobs before processing, so reliable queues need an explicit pending/ack pattern or Streams; persistence and retry policy still matter.
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.
- Effective validity subtracts acquisition elapsed time and a clock-drift safety allowance from the original TTL; work must finish within that remaining lease.
- 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, background writing, but fork/CoW can affect latency.
- − 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, typically about a second of recent writes can be lost),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.
Choice: match persistence to the data-loss budget. With both RDB and AOF enabled, Redis normally loads AOF at restart. Multi-part AOF (Redis 7+) can use an RDB-format base plus incremental AOF files; this is distinct from simply enabling independent RDB snapshots. Test restoration and budget for fork/CoW memory and storage failures.
⚠️ 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 is the eviction accounting threshold, not a hard cap on process RSS. At the threshold Redis evicts eligible keys under the configured policy or rejects commands that require more memory. Buffers, fragmentation, temporary overshoot and fork/CoW require additional headroom.
In detail:
The maxmemory 2gb parameter + maxmemory-policy <policy>. Common policies:
| Policy | What it does |
|---|---|
noeviction |
No eviction; memory-growing commands may fail, while reads/deletes can continue |
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. Compare it with LRU against the actual access pattern.
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.
Sentinel quorum is the number of observers agreeing the primary is down. Authorizing a failover additionally requires a majority of Sentinel processes, so a configured quorum alone is not enough in a network minority. Redis Cluster replicas must be provisioned; sharding alone does not create HA.
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 write path can keep cached values current when every writer follows the protocol. It does not make separate DB/cache writes atomic; partial failure and racing fills still need handling.
- − 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 — synchronous write coordination can improve freshness, but separate stores still need an explicit partial-failure and concurrency protocol.
- 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.
- With exactly one cache lookup per read and one DB query per miss, a 90% hit ratio reduces that read-query volume to 10%. Multiple keys, write traffic and expensive misses change the relation; measure saved DB work, not just hits.
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 stampede happens when many requests miss the same popular entry and recompute it together. Coalesce work, refresh early and bound the load reaching the source.
A safe cache-fill protocol:
- Read the cache; distinguish a missing value from valid values such as zero or an empty list.
- Coalesce concurrent misses in one process with single-flight. Across processes, an optional Redis lease uses
SET lock:key unique_token NX PX ttl. - After winning the lease, check the cache again, then compute under a deadline and a bounded source concurrency limit.
- Release with an atomic compare-token-and-delete script. A plain DEL can delete a successor's lease after yours expires.
- Other callers wait only up to their deadline, serve permitted stale data, or fail fast. Do not recursively retry without a bound.
Lease expiry can still allow duplicate computation. Cache fills that must preserve freshness need version checks against stale writers; business correctness must not depend only on this lease.
TTL jitter spreads expiry across different keys. Probabilistic early refresh and background refresh reduce misses but do not guarantee that an entry never expires. Stale-while-revalidate is appropriate only within the product's allowed stale-data budget.
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.
Bloom filters have no false negatives for the items actually inserted. A stale or rebuilding filter may omit a newly created database ID: update it before exposing that ID or fall back to the source when coverage is uncertain. The false-positive rate is configurable, not always 1%.
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? — via expiration, validation, or 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.
PostgreSQL does not have a general query-result cache by default: shared_buffers and the OS page cache retain pages, not arbitrary SELECT results. HTTP validation (ETag/Last-Modified) can refresh a browser entry without transferring the body; URL versioning is not the only refresh mechanism.
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 — a binary document format with ObjectId, Date, Decimal128 and binary types. It can be larger than equivalent JSON because it also stores lengths and type information; compactness and speed are workload-dependent.
// 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.
Flexible schema does not mean no validation: a collection can enforce validators, required fields and types. The BSON document limit is 16 MiB.
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: stage order affects both meaning and speed. Moving LIMIT before GROUP or SORT can change the answer; move it only when equivalent. Push selective MATCH conditions early when valid. MongoDB may move eligible filters itself, and a later textual MATCH can still participate in an optimized indexed plan. Inspect explain instead of assuming stage position proves index use.
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"
}}
])
Modern MongoDB can optimize eligible equality lookups (including its slot-based execution engine). Cost depends on cardinality, indexes on the foreign join fields and pipeline shape; it is not universally a nested loop or slower than SQL. Check explain on the deployed version.
⚠️ Gotcha: resharding is supported but is an operational migration. Monotonic keys can concentrate new writes with range sharding; hashed sharding distributes values differently but changes range-query locality. Evaluate cardinality, frequency, monotonicity and query targeting together.
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: duplication makes updates and consistency harder. MongoDB also supports references and normalized relationships; use them for independently accessed or frequently updated data and unbounded arrays. Embedding is strongest when related data has bounded size, shared lifecycle and is usually read/updated together.
25Why have a cache at all, if a database with indexes is already fast?
concept
Short answer: An index speeds up finding rows; a result cache can avoid repeating the query, aggregation, serialization or remote call altogether. Whether that helps must be measured.
A cache is useful when many reads reuse the same result, source capacity is scarce, recomputation is expensive, and some staleness is acceptable. Example: caching a popular dashboard aggregate can remove repeated joins and aggregation work even though every join is indexed.
Do not assume a fixed 10–100× speedup: PostgreSQL also caches pages in RAM, and an extra Redis network hop plus serialization can outweigh a cheap indexed query. Hit ratio, miss cost, freshness, memory and failure behavior determine the benefit.
⚠️ Gotcha: a cache adds invalidation and operational work. Plan bounded fallback and gradual warming so a cold or unavailable cache cannot overwhelm the source. If measured database latency and capacity already meet the target, a cache may add more complexity than value.
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
Source links provide further reading. Where a recorded technical review is available, the answer shows its date and scope. Tool selections use direct provider links and contain no affiliate placements. Sources, features, and hiring formats can change.
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.