Redis — Interview Questions

Stack context: This system uses Redis 7 for three distinct patterns: (1) session store — user-service writes session:{jwt} on login; api-gateway reads on every request; (2) cache-aside — product-service caches product:{uuid} with 600 s TTL; (3) rate limiting — Spring Cloud Gateway token-bucket rate limiter. A Caffeine in-memory cache (60 s, 10k entries) provides resilience when Redis is unavailable.


Q1 — What is Redis and what makes it different from a relational database? junior

Answer: Redis (Remote Dictionary Server) is an in-memory data structure store that can act as a cache, database, message broker, and queue. Unlike relational databases:

Aspect Redis Relational DB
Storage Primary: RAM (optional disk persistence) Primary: Disk
Data model Key-value with rich types (string, hash, list, set, sorted set, stream) Tables with rows and columns
Query language Commands per data type SQL
Latency Sub-millisecond 1–100 ms (disk I/O dependent)
Persistence Optional (RDB snapshots, AOF log) Always durable
Transactions MULTI/EXEC (limited) ACID full

Use case: Redis excels as a complement to a relational database — store frequently read, rarely changed data in Redis for microsecond latency; store authoritative data in PostgreSQL.


Q2 — What is the cache-aside (lazy loading) pattern? junior

Answer: Cache-aside means the application code manages cache population, not the database or cache infrastructure.

Flow:

  1. Application checks cache for the data.
  2. Cache HIT: Return data from cache immediately.
  3. Cache MISS: Fetch from the database, write to cache with TTL, return data.
  4. On update: Write to database, then invalidate (or update) cache.
// product-service: ProductService.java
public ProductDto getProduct(UUID id) {
    String cached = redisTemplate.opsForValue().get("product:" + id);
    if (cached != null) {
        return objectMapper.readValue(cached, ProductDto.class); // HIT
    }
    ProductDto product = productRepository.findById(id).orElseThrow();
    redisTemplate.opsForValue().set("product:" + id, serialize(product), Duration.ofSeconds(600));
    return product; // MISS → populated
}

This system: product-service sets X-Cache: HIT or X-Cache: MISS response header for observability.


Q3 — What is a Redis TTL and why is it important? junior

Answer: TTL (Time-To-Live) is the expiration time for a Redis key in seconds. When the TTL expires, Redis automatically deletes the key.

Why it matters:

Commands:

SET session:abc123 {...} EX 1800    # set with 1800s TTL
EXPIRE session:abc123 1800          # set TTL on existing key
TTL session:abc123                  # check remaining TTL (-2 = expired/not found)
PERSIST session:abc123              # remove TTL (make persistent)

This system: Sessions use 1800 s (30 min) TTL matching JWT expiry. Products use 600 s TTL.


Q4 — What Redis data types do you know and when would you use each? junior

Answer:

Type Use Cases
String Simple key-value: session data, cached objects (JSON), counters (INCR)
Hash Store object fields individually: user profile HSET user:1 name "Alice" email "..."
List Queues (LPUSH/RPOP), activity feeds, recent items
Set Unique members: tags, user follows, deduplication
Sorted Set Leaderboards, priority queues, range queries by score
Stream Event log, producer-consumer queues with consumer groups
Bitmap User activity tracking (DAU/MAU) with minimal memory
HyperLogLog Approximate unique counts (cardinality)

This system: Strings for session store and product cache. The rate limiter uses a Sorted Set (Spring's RedisRateLimiter uses ZADD/ZREMRANGEBYSCORE internally for the token bucket).


Q5 — What is Redis eviction and how do you configure it? junior

Answer: When Redis reaches its maxmemory limit, it must decide what to do with new writes. The maxmemory-policy setting controls the eviction strategy:

Policy Behavior
noeviction Return error on write; never evict (default for persistence)
allkeys-lru Evict least recently used keys across all keys
volatile-lru Evict LRU among keys with TTL set
allkeys-lfu Evict least frequently used (Redis 4+)
allkeys-random Evict random key
volatile-ttl Evict key with shortest remaining TTL

Cache configuration: Use allkeys-lru so Redis automatically evicts cold cache entries when full, keeping hot data in memory.

Session store configuration: Use noeviction or volatile-lru so active sessions are never silently dropped — users should be logged out explicitly.


Q6 — What is the difference between Redis SET, SETEX, SETNX, and SET ... NX EX? junior

Answer:

Command Behavior
SET key value Set unconditionally
SETEX key seconds value Set with TTL (older API)
SETNX key value Set only if key does NOT exist (atomic)
SET key value NX EX seconds Set only if NOT exists + TTL (modern, atomic)
SET key value XX EX seconds Set only if EXISTS + TTL

SET ... NX EX is the correct distributed lock primitive:

SET lock:order-123 "worker-a" NX EX 30

If this returns OK, you hold the lock for 30 s. If it returns nil, another process holds it. This atomically combines the existence check and TTL in a single command — avoiding the TOCTOU race of SETNX + EXPIRE.


Q7 — How does Redis handle persistence and what are the trade-offs of RDB vs AOF? junior

Answer:

RDB (Redis Database) Snapshots:

AOF (Append-Only File):

Combined: Use both — RDB for fast restarts, AOF for durability. Redis will use AOF for recovery if both exist.

This system: Docker development uses RDB. Production session store would need AOF everysec.


Q8 — What is a Redis pipeline and when should you use it? junior

Answer: A pipeline batches multiple Redis commands into a single network round-trip. Instead of sending N commands and waiting for N replies, you send all N commands at once and receive all N replies together.

// Without pipeline: N round-trips
for (String id : productIds) {
    redisTemplate.opsForValue().get("product:" + id);
}

// With pipeline: 1 round-trip
List<Object> results = redisTemplate.executePipelined(connection -> {
    for (String id : productIds) {
        connection.get(("product:" + id).getBytes());
    }
    return null;
});

When to use:

When NOT to use:


Q9 — What is a distributed lock and how is it implemented in Redis? senior

Answer: A distributed lock prevents multiple processes from concurrently executing a critical section. Redis is a common choice because it's fast, atomic, and shared across services.

Basic implementation (Redlock for single instance):

SET lock:{resource} {unique_id} NX EX {timeout}

Release (Lua script for atomic check-and-delete):

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

Redlock (multi-node algorithm by Redis author): Acquire lock on majority of N (≥3) independent Redis nodes. A lock is valid if acquired on the majority within TTL/2 time.

This system: OutboxPublisher uses a single-node lock to prevent multiple pods from double-publishing outbox events simultaneously.


Q10 — How does Spring's RedisRateLimiter work and what algorithm does it use? senior

Answer: Spring Cloud Gateway's RedisRateLimiter implements the token bucket algorithm using a Lua script executed atomically on Redis.

Token bucket concept:

Lua script (executed atomically):

  1. Compute elapsed time since last request using Redis TIME.
  2. Add elapsed × replenishRate tokens to the bucket.
  3. Cap at burstCapacity.
  4. If bucket ≥ requestedTokens, subtract and allow; else deny.

Redis keys (per user/IP):

This system: api-gateway applies rate limiting per X-User-Id header. Authenticated users get a higher burst capacity than anonymous requests. Redis handles this statelessly across gateway replicas.


Q11 — What is the N+1 cache problem and how do you solve it? senior

Answer: The N+1 cache problem occurs when loading a list of items:

  1. Load a list of N IDs from the database.
  2. For each ID, check the cache individually (N round-trips).
  3. For each MISS, load from the database (N additional queries).

Solutions:

Multi-get (pipeline):

List<String> keys = ids.stream().map(id -> "product:" + id).collect(toList());
List<Object> values = redisTemplate.opsForValue().multiGet(keys); // 1 round-trip

Cache warming: Pre-populate cache on startup or via an async background job.

Batch DB fallback: Collect all MISS keys, load from DB in a single IN (...) query, populate cache in a pipeline.

List<UUID> misses = findMisses(ids, cachedValues);
if (!misses.isEmpty()) {
    List<Product> dbResults = productRepo.findAllById(misses);
    batchCacheWrite(dbResults); // pipeline
}

Q12 — Explain Redis Sentinel vs Redis Cluster and when to use each. senior

Answer:

Redis Sentinel:

Redis Cluster:

Decision criteria:

This system: Single Redis node (development). Production would use Sentinel for HA, Cluster for scale.


Q13 — What is the Caffeine fallback and why is it needed alongside Redis? senior

Answer: The Caffeine fallback is a JVM-local in-memory cache (per service instance) that stores recently validated sessions. It addresses two problems:

1. Redis latency: Every API Gateway request hitting Redis adds ~1 ms. With 100k req/s, Redis becomes the bottleneck.

2. Redis unavailability: If Redis goes down (network blip, rolling restart), ALL gateway requests would fail with 401/500 without a fallback.

Implementation in this system:

// api-gateway: SessionValidationFilter.java
Caffeine.newBuilder()
    .expireAfterWrite(60, SECONDS)   // 60-second local TTL
    .maximumSize(10_000)             // 10k concurrent sessions
    .build();

Flow:

  1. Check Caffeine cache → HIT: return immediately (no Redis call).
  2. Caffeine MISS → check Redis → HIT: populate Caffeine, return.
  3. Redis unavailable → serve Caffeine for up to 60 s for known sessions.

Trade-off: 60-second window where a revoked session (logout) is still accepted from Caffeine. Acceptable for most use cases; reduce TTL if stricter revocation is needed.


Q14 — How do Redis transactions work with MULTI/EXEC and what are their limitations? senior

Answer: Redis MULTI/EXEC queues commands and executes them atomically as a batch:

MULTI
SET session:abc "active"
INCR login_count:user:1
EXEC  → [OK, (integer) 5]

Guarantees:

Limitations:

Better alternative for conditional logic: Lua scripts — atomic, support branching, can read and write in the same script. Used by Spring's RedisRateLimiter and distributed lock release.


Q15 — What are common Redis memory optimization techniques? senior

Answer:

  1. Use appropriate data types: Use Hash for objects with multiple fields instead of multiple String keys.

    • Bad: SET user:1:name "Alice", SET user:1:email "..." (2 keys overhead each)
    • Good: HSET user:1 name "Alice" email "..." (1 key, hash encoding if <128 fields)
  2. Hash encoding thresholds: Redis uses a compact ziplist encoding for small hashes (< hash-max-ziplist-entries = 128 by default) → significant memory savings.

  3. Set appropriate TTLs: Ensure all cache keys have TTLs to prevent unbounded growth.

  4. Compression at app layer: Store gzip-compressed JSON for large cached objects.

  5. Key naming: Short, consistent key names reduce memory overhead. Use p: instead of product: for high-volume keys.

  6. redis-memory-doctor: CLI tool for memory usage analysis and optimization suggestions.

  7. OBJECT ENCODING key: Inspect actual encoding to verify optimization strategies are effective.


Q16 — How would you handle cache stampede (thundering herd) in Redis? senior

Answer: A cache stampede occurs when many requests simultaneously find the same cache key expired, all go to the database concurrently, and all try to repopulate the cache at the same time — overwhelming the database.

Solutions:

1. Probabilistic early expiration: Before the TTL actually expires, a small fraction of requests re-fetch proactively. Prevents simultaneous expiry of many requests.

2. Mutex / lock on MISS:

String cached = redis.get(key);
if (cached == null) {
    boolean lockAcquired = redis.set("lock:" + key, "1", NX, EX, 10);
    if (lockAcquired) {
        cached = db.fetch(id);
        redis.set(key, cached, EX, 600);
        redis.del("lock:" + key);
    } else {
        // Wait briefly and retry — another thread is populating
        Thread.sleep(50);
        cached = redis.get(key);
    }
}

3. Background refresh: Serve stale data from cache while asynchronously refreshing. stale-while-revalidate pattern.

4. Jitter on TTL: Add random offset to TTL to prevent mass simultaneous expiry of related keys (e.g., TTL = 600 + random(0, 60)).


Q17 — How do you implement a sliding window rate limiter in Redis? senior

Answer: Using a Sorted Set where the score is the timestamp:

-- Lua script (atomic)
local key = KEYS[1]          -- "ratelimit:user:123"
local now = tonumber(ARGV[1]) -- current timestamp in ms
local window = tonumber(ARGV[2]) -- window size in ms (e.g., 60000 = 1 min)
local limit = tonumber(ARGV[3])  -- max requests per window

-- Remove entries older than window
redis.call("ZREMRANGEBYSCORE", key, 0, now - window)

-- Count current requests in window
local count = redis.call("ZCARD", key)

if count < limit then
    -- Add current request
    redis.call("ZADD", key, now, now .. math.random())
    redis.call("EXPIRE", key, math.ceil(window / 1000))
    return 1  -- allowed
else
    return 0  -- denied
end

vs. Token bucket (Spring's approach): Token bucket smooths traffic by replenishing at a constant rate. Sliding window is exact but uses more memory (stores all request timestamps). Token bucket uses O(1) memory per key.


Q18 — What is Redis pub/sub and how does it differ from Kafka? senior

Answer: Redis pub/sub is a fire-and-forget messaging channel. Publishers send messages to a channel; all current subscribers receive them.

SUBSCRIBE order.notifications
PUBLISH order.notifications "Order #123 shipped"

Key differences from Kafka:

Feature Redis Pub/Sub Kafka
Persistence No — missed messages are lost Yes — retained for retention period
Delivery guarantee At-most-once At-least-once or exactly-once
Consumer groups No — all subscribers get all messages Yes — group-based partitioned consumption
Replay No Yes — seek to any offset
Scale Single node (Cluster = keyslot limitations) Horizontally scalable

Use Redis pub/sub for: Cache invalidation broadcast, real-time chat, lightweight notifications where loss is acceptable.

Use Kafka for: Any durable, replayable event streaming (order processing, audit logs, etc.).

This system: Uses Kafka for all inter-service events. Redis pub/sub is not used but would be appropriate for broadcasting cache invalidation across product-service instances.


Q19 — How would you implement a leaderboard with Redis? junior

Answer: Redis Sorted Sets are purpose-built for leaderboards. The score is the numeric value (points, sales, etc.); the member is the user identifier.

// Add/update score
redisTemplate.opsForZSet().add("leaderboard:weekly", "user:123", 1500.0);

// Increment score
redisTemplate.opsForZSet().incrementScore("leaderboard:weekly", "user:123", 50.0);

// Get top 10 (highest score first)
Set<ZSetOperations.TypedTuple<String>> top10 =
    redisTemplate.opsForZSet().reverseRangeWithScores("leaderboard:weekly", 0, 9);

// Get user rank (0-indexed, lowest = rank 1)
Long rank = redisTemplate.opsForZSet().reverseRank("leaderboard:weekly", "user:123");

Time complexity: ZADD O(log N), ZRANGE O(log N + M). All leaderboard operations are O(log N) — extremely efficient even for millions of users.

This system: Not currently used, but order-service could maintain orders:count:customer:{id} sorted set for customer activity rankings.


Q20 — How do you monitor Redis in production? junior

Answer: Key monitoring commands and metrics:

Real-time monitoring:

redis-cli MONITOR        # stream all commands (dev only — high overhead)
redis-cli INFO           # comprehensive stats
redis-cli INFO memory    # memory usage, fragmentation ratio
redis-cli INFO stats     # keyspace hits/misses, ops/sec
redis-cli INFO clients   # connected clients, blocked clients

Key metrics to track:

Production tooling: Redis Exporter → Prometheus → Grafana dashboard. Alert on: memory > 80%, hit ratio < 80%, rejected connections > 0.


Q21 — What is Redis keyspace notification and how would you use it? senior

Answer: Keyspace notifications allow clients to subscribe to events on Redis keys via pub/sub channels. Enabled with notify-keyspace-events config.

Event types (configured as flags):

Example — session expiry audit:

// Subscribe to expired events
redisTemplate.convertAndSend("__keyevent@0__:expired", ...);
// In listener: log session_id, user_id, expiry_time to audit table

Use cases:

Warning: High-frequency keyspace notifications add overhead. Use selectively and only subscribe to necessary event types.


Q22 — What is the Redis object encoding and why does it matter for performance? senior

Answer: Redis uses different internal encodings for data types based on size/content for memory efficiency:

Type Small (ziplist/listpack) Large (hash table/skiplist)
Hash ziplist/listpack (< 128 fields, < 64 bytes/field) hashtable
List listpack (< 128 elements) quicklist
Sorted Set listpack (< 128 members) skiplist + hashtable
Set (integers) intset (integer-only members) hashtable
String int, embstr (≤44 bytes), raw

Performance implications:

Tuning: If you have many small hashes, ensure thresholds aren't crossed unnecessarily. hash-max-listpack-entries 128 and hash-max-listpack-value 64 control the cutoff.

Inspection: OBJECT ENCODING product:abc123 → verify embstr for small JSON values.


Q23 — How do you handle Redis connection pooling in Spring? junior

Answer: Spring Data Redis uses Lettuce (async) or Jedis (sync) as the client. Lettuce uses a single multiplexed connection per thread pool (non-blocking), while Jedis uses a traditional connection pool.

Lettuce (default in Spring Boot):

spring:
  redis:
    lettuce:
      pool:
        max-active: 8      # max connections
        max-idle: 8        # max idle connections kept alive
        min-idle: 2        # min connections maintained
        max-wait: -1ms     # wait indefinitely for connection (-1 = block)

Jedis pool (if using Jedis):

spring:
  redis:
    jedis:
      pool:
        max-active: 8
        max-wait: 2000ms   # timeout after 2s — better than blocking forever

Best practice with Lettuce: Lettuce's multiplexed connection handles thousands of concurrent requests on a single connection. Pooling is optional but useful for SSL or blocking commands (BLPOP).

This system: Uses Lettuce (Spring Boot default). ReactiveRedisTemplate in the api-gateway (WebFlux) uses Lettuce's non-blocking reactor integration natively.


Q24 — Explain the difference between ReactiveRedisTemplate and RedisTemplate. junior

Answer:

RedisTemplate ReactiveRedisTemplate
Programming model Blocking (synchronous) Non-blocking (Project Reactor Mono/Flux)
Thread usage One thread per request (Tomcat model) Event loop (Netty), fewer threads
Use with Spring MVC, RestTemplate Spring WebFlux, WebClient
API opsForValue().get(key) → value directly opsForValue().get(key)Mono<V>

This system:

Mixing RedisTemplate in a WebFlux app blocks the event loop — avoid it. Always use ReactiveRedisTemplate in reactive stacks.


Q25 — What are Redis Streams and how do they compare to Kafka? senior

Answer: Redis Streams (introduced in Redis 5.0) are a persistent, append-only log with consumer groups — similar to a lightweight Kafka.

Features:

vs. Kafka:

Redis Streams Kafka
Persistence Redis memory + optional AOF Disk (TB scale)
Throughput Hundreds of thousands msg/s Millions msg/s
Retention Trim with MAXLEN; memory-bound Days/weeks on disk
Replication Replica sync (Sentinel/Cluster) Configurable replication factor
Ecosystem Basic Rich (Kafka Streams, Connect, MirrorMaker)

Use case: Redis Streams work for lightweight, low-latency, low-volume event pipelines within a bounded retention window. Kafka is better for high-throughput, durable event archives.


Q26 — How would you invalidate a cache when data changes in the database? senior

Answer: Cache invalidation is one of the hardest problems in distributed systems. Strategies:

1. Delete on write (eager invalidation):

productRepository.save(product);            // update DB
redisTemplate.delete("product:" + product.getId()); // invalidate cache

Simple, but subject to race condition: if another request populates cache between save and delete, you have a stale entry.

2. Update on write (write-through):

productRepository.save(product);
redisTemplate.opsForValue().set("product:" + id, serialize(product), TTL);

Cache always has fresh data. Risk: if DB write succeeds but Redis write fails, cache is stale until TTL expires.

3. CDC (Change Data Capture) + event-driven invalidation:

4. TTL-based staleness acceptance: For read-heavy data where minor staleness (< 10 min) is acceptable, just let TTL expire naturally.

This system: Uses delete-on-write (redisTemplate.delete(...)) after product updates.


Q27 — What is Redis Cluster hash slot and how does key routing work? senior

Answer: Redis Cluster divides the keyspace into 16,384 hash slots (0–16,383). Each master node owns a range of slots. A key is assigned to a slot using:

slot = CRC16(key) % 16384

The client computes the slot and connects directly to the correct node.

Hash tags enable multi-key operations on the same node:

Resharding: When adding/removing nodes, slots (and their data) are migrated. During migration, keys may be on either source or destination node; the cluster redirects with ASK/MOVED responses.

Client support: Lettuce and Jedis both handle MOVED/ASK transparently, reconnecting to the correct node automatically.


Q28 — How do you warm up a Redis cache after a cold start or deployment? senior

Answer: Cache warm-up prevents thundering herd on a fresh deployment where all requests are cache misses.

Strategies:

1. Pre-load on startup (Spring ApplicationRunner):

@Component
public class ProductCacheWarmer implements ApplicationRunner {
    public void run(ApplicationArguments args) {
        List<Product> topProducts = productRepo.findTop1000ByOrderBySalesDesc();
        topProducts.forEach(p -> cache.put("product:" + p.getId(), serialize(p)));
    }
}

2. Lazy warm-up with shadow traffic: Route a small percentage of production traffic to the new instance before switching. Cache warms from real requests.

3. Snapshot restore: Export cache from previous instance (BGSAVE + copy RDB) and import on new instance.

4. Read-through proxy: Cache proxy (e.g., Twemproxy) transparently populates on MISS. No app code needed.

This system: Product service would benefit from warming the top 1,000 products on startup, reducing cold-start cache miss rate.


Q29 — What is the Redis memory fragmentation ratio and when is it a problem? senior

Answer: Memory fragmentation occurs when Redis allocates memory blocks that aren't fully utilized. The fragmentation ratio is:

fragmentation_ratio = used_memory_rss / used_memory

Interpretation:

Causes of high fragmentation:

Remediation:


Q30 — How would you design a high-availability Redis setup for a critical session store? senior

Answer: For a critical session store, availability and durability are the primary concerns.

Architecture:

[App Instances] → [Lettuce Client (Sentinel-aware)]
                          ↓
              [Redis Sentinel Quorum: 3 nodes]
                          ↓
           [Master] ←sync→ [Replica 1] ←sync→ [Replica 2]
              ↑ failover promotion on master failure

Configuration checklist:

Client failover: Lettuce automatically reconnects to the new master after Sentinel elects it (typically < 30 s). Application should implement retry logic for the failover window.

This system: Single-node Redis for development. The architecture above is the production target for the session store.