Skip to content
Backend & systems

26 NoSQL and Redis Interview Questions and Answers

This focused guide turns RecallDeck’s curated NoSQL and Redis material into 26 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

31 min read26 detailed answersReviewed Aug 24, 2026
What to remember

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

01

What types of NoSQL databases are there, and when do you choose each?

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.

02

When should you choose SQL and when NoSQL?

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.

03

When should you NOT reach for NoSQL?

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.

04

Explain the CAP theorem. Why can't you have it all at once?

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.

05

What's the difference between ACID and BASE?

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.

06

What are eventual consistency and strong consistency?

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.

07

What is Redis?

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.

08

Why is Redis single-threaded yet so fast?

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:

  1. Data in RAM — no disk I/O during operations (memory access is orders of magnitude faster than disk).
  2. No locks or contention — one thread = no mutexes, no cache-line bouncing between cores, no deadlocks.
  3. I/O multiplexing — epoll/kqueue handles thousands of connections in a single loop without a thread per connection.
  4. Efficient data structures — optimized implementations (skip lists for ZSet, special encodings for small collections).
  5. 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.

09

What data structures does Redis have, and what is each for?

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.

10

What are the typical use cases for Redis?

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. QueuesLPUSH + 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 locksSET 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.

11

How do you make a distributed lock in Redis? What is Redlock?

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):

  1. Get the current time.
  2. Sequentially try to acquire the lock on all N nodes with the same token and a small timeout.
  3. The lock is considered acquired if it was obtained on a majority (N/2+1) of nodes AND the total time < TTL.
  4. The effective TTL = the original TTL minus the time spent.
  5. 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.

12

How does Redis persist data to disk? RDB vs AOF.

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 schedule save 900 1).
  • BGSAVE forks 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.
  • fsync policies: 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.

13

What are eviction policies in Redis and how does maxmemory work?

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.

14

What's the difference between replication, Sentinel, and Cluster in Redis?

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.

15

What caching patterns are there? Cache-aside, read/write-through, write-behind.

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.

16

What's so hard about cache invalidation? ("one of the two hard things in CS")

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:

  1. 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.
  2. Multiple copies: the same data may sit in the application cache, Redis, a CDN, the browser — you need to invalidate everywhere.
  3. Dependencies: changing one entity may affect many derived caches (aggregates, lists, denormalized views). It's hard to track "what went stale."
  4. 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.

17

What is cache hit ratio and why does it matter?

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."

18

What is a cache stampede / thundering herd and how do you deal with it?

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:

  1. 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)
  1. 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.
  2. Background refresh — a background worker refreshes hot keys on a schedule, so the cache "never goes empty."
  3. 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).

19

What are cache penetration, cache avalanche, and a hot key?

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.

20

Where can you cache data? What cache levels are there?

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.

21

What is MongoDB? Documents, collections, BSON.

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.

22

How do indexes and aggregations (aggregation pipeline) work in MongoDB?

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.

23

How does MongoDB scale? Replica set, sharding. Is there a join?

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) and readConcern control 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.

24

What is denormalization in NoSQL and query-driven design?

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:

  1. First we determine the queries the application needs.
  2. We design the tables/documents around those queries so that each one is served by a single read.
  3. 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.

25

Why have a cache at all, if a database with indexes is already fast?

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:

  1. 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.
  2. 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.
  3. 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.
  4. Protection against spikes: the cache smooths out spike traffic (a viral post), keeping it from crashing the database.
  5. 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.

26

If the cache is a separate component, doesn't it make the system less reliable?

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.

Start studying

Keep going

RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS