Every senior backend interview I've done in the last 18 months asked at least one Redis question and one Kafka question. Below are the 10 that came up most often, answered with the patterns I actually use in production — not the textbook version.
0. Context (read this once, skip on reread)
These answers come from PSI, a POS + inventory microsystem I built over 18 months. It runs 12 Spring Boot services and 3 Go services across 648 APIs, deployed for small retailers in Zambia, the UK, and China. The architecture is deliberately boring — Redis for cache, Kafka for async events — because boring architectures ship on time.
If you only want the TL;DR per question, each section starts with one. If you want the war stories, keep reading.
1. "Why is Redis so fast if it's single-threaded?"
TL;DR: Single-threading isn't the bottleneck — it's the feature. One thread = no lock contention = O(1) for almost everything.
The textbook answer is "memory + I/O multiplexing + single thread." True but useless in interviews. The real question is: what did you optimize away by going single-threaded?
You optimized away:
- Lock contention on data structures (single thread, single owner)
- Context switches between threads (one less source of latency spikes)
- Cache line bouncing across cores (one CPU, one L1, no coherency traffic)
That's why GET/SET stay at sub-millisecond even under 100K QPS.
What this looked like in PSI
In psi-goods, the SKU master cache hit the same 80% of SKUs 95% of the time. With a thread-per-connection model (like a naive JDBC server), we'd lock the dict on every miss. With Redis single-thread, miss storms just queue up — they don't deadlock. We ran 4KB average value, 2M keys on a 8GB instance, p99 < 4ms.
The cost if you get it wrong
I picked Memcached once in 2018 because someone on a blog said "Memcached is faster on benchmarks." Six months later I wanted SCAN, persistence, and Pub/Sub. Three months of migration back. The benchmarks were right; the blog forgot to mention what Memcached can't do.
When interviewers actually care
They're listening for whether you know when single-thread breaks:
-
KEYS */SMEMBERSon large collections (blocks the single thread) - Big key
DEL(frees memory slowly, blocks) - Long-running Lua scripts (blocks for the duration)
If you only know the textbook answer, you sound like you read it yesterday.
2. "Walk me through the three cache problems."
TL;DR: Penetration (null doesn't exist), Avalanche (thundering herd at expiry), Breakdown (hot key expires, dogpile on DB).
Don't list them. Show me the code.
PSI scenario: QR-code scan-to-order
Each scan pings psi-cashier, which checks SKU via psi-goods. If the SKU isn't cached AND isn't in the DB, we still need a fast answer ("not found") instead of letting 1,000 concurrent scans stampede the DB.
The fix — three lines, three problems
// 1. Penetration — cache the "negative" too
Sku sku = redis.get("sku:" + code);
if (sku == null) {
sku = db.query(code);
if (sku == null) {
redis.setex("sku:" + code, 300, "NULL_TOKEN"); // 5-min null cache
return null;
}
redis.setex("sku:" + code, 3600, JsonUtil.toJson(sku));
}
// 2. Breakdown — single-flight via distributed lock
Sku cached = redis.get("sku:" + code);
if (cached != null) return cached;
String lockKey = "lock:sku:" + code;
if (redis.set(lockKey, "1", "NX", "EX", 5) != null) { // got the lock
try {
Sku fresh = db.query(code);
redis.setex("sku:" + code, 3600, JsonUtil.toJson(fresh));
return fresh;
} finally {
redis.del(lockKey);
}
}
Thread.sleep(50); cache = redis.get(...); // wait + retry
// 3. Avalanche — random jitter on TTL
long baseTtl = 3600;
long jitter = ThreadLocalRandom.current().nextLong(0, 600);
redis.setex("sku:" + code, baseTtl + jitter, JsonUtil.toJson(sku));
The cost if you get it wrong
In 2024, a Black Friday promotion hit psi-goods with 200 QPS on the same SKU. The SKU key had no jitter, expired at the same second. 200 concurrent queries stampeded Postgres. CPU went 100% for 90 seconds. Stores stopped scanning for a minute and a half. Customers walked out. Owner called me at 11 PM. That's the cost.
3. "When would you not use Redis?"
TL;DR: When the data is local-only AND changes rarely AND every node needs to see it eventually consistent.
This is the question junior candidates fail. They answer "use Redis everywhere."
PSI scenario: permission tree
In psi-system, every API checks the caller's role against the menu/permission tree. The tree has 200 nodes. It changes maybe once a week. Redis is overkill — the round-trip to Redis is 1ms; the code execution is 0.1ms; the network variance adds 5ms p99 spikes.
The actual architecture
// Caffeine local cache + Redis pub/sub invalidation
LoadingCache<String, PermissionTree> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(key -> redis.get("perm:" + key));
// One node updates → publish
redisTemplate.convertAndSend("perm-invalidate", "all");
// Every node listens
@EventListener
public void onInvalidate(String msg) {
localCache.invalidateAll();
}
Result: 4-microsecond reads on the hot path, 5-minute consistency window across 30 stores. The cost of inconsistency is zero because the permission tree almost never changes.
The cost if you get it wrong
Use Redis for everything and your middleware call graph becomes spaghetti. Now you're debugging a 5-second window where some stores have an old menu after a global promotion — and the customer service team is in your inbox.
4. "Why Kafka and not RabbitMQ?"
TL;DR: Because I need to replay the past. RabbitMQ deletes after ack; Kafka keeps the log.
Interviewers ask this to test if you understand the model difference, not "which is faster."
PSI scenario: daily close audit
Every night, psi-finance runs daily close — it crunches the day's sales, tax, refunds, and cash drawer variance. If the auditor needs to re-run close for the last 30 days (e.g., tax rate changed retroactively), I need to replay those 30 days of events.
With Kafka, I just reset the consumer offset to 30 days ago and re-run. With RabbitMQ, those events are gone.
When RabbitMQ is better
- Single-shot task distribution ("send this email")
- Per-message routing that changes frequently
- Strict per-message ordering across all consumers (RabbitMQ's queue model is naturally strict)
If your system is "fire event, expect consumer to handle it once, forget it" — RabbitMQ wins on simplicity. If your system is "fire event, keep for audit, may need to replay" — Kafka wins.
The cost if you get it wrong
Picked RabbitMQ for daily close because "it's simpler." Six months in, regulator changes the tax rate for the last quarter. We have no way to recompute historical close. We open the DB, write a SQL script, pray. (Yes, I did this in 2022.)
5. "How do you guarantee exactly-once?"
TL;DR: You don't. You guarantee at-least-once + idempotent consumer. Exactly-once is marketing.
The "transactional producer + read-committed consumer" combo gets effectively exactly-once, but the language is misleading.
PSI scenario: refund + inventory sync
When a customer asks for a refund, psi-finance issues the refund, then must restock the item in psi-goods. If the refund is processed but inventory isn't restocked → money gone, stock gone. Worst case scenario.
The implementation
// Producer: idempotent (retries don't duplicate)
@Bean
public ProducerFactory<String, RefundEvent> refundProducerFactory() {
return new DefaultKafkaProducerFactory<>(props, ..., new JsonSerializer<RefundEvent>()) {{
put("enable.idempotence", "true"); // broker dedupes by producer-id + sequence
put("acks", "all");
put("max.in.flight.requests.per.connection", "5");
}};
}
// Consumer: idempotent via event_id check
@KafkaListener(topics = "refund-events")
public void onRefund(RefundEvent e) {
String dedupKey = "refund:processed:" + e.eventId();
if (redis.setIfAbsent(dedupKey, "1", Duration.ofDays(7))) {
financeService.reversePayment(e.orderId(), e.amount());
goodsService.restock(e.sku(), e.qty());
}
// already processed — silently drop
}
Two layers: producer idempotence kills network retries, consumer idempotence kills redelivery.
The cost if you get it wrong
Caught this exact bug in production last year. A network blip caused the consumer to crash mid-processing. The broker redelivered. The inventory was double-restocked. We had 200 bottles of Coca-Cola on the shelf that didn't really exist. Audit was a nightmare.
6. "What happens when a Kafka consumer group rebalances?"
TL;DR: Everything stops. 30 seconds to a few minutes if you're not careful.
PSI scenario: psi-report sales dashboard
The sales report consumer reads from 3 topics (orders, payments, refunds) and writes a per-store daily rollup to Postgres. It runs on 6 consumer instances for parallelism. When we added a 7th instance at noon, all 6 existing consumers paused, partitions shuffled, processing resumed.
Result: 40 seconds of lag. The owner opened the dashboard during the lag — saw numbers from 12:00 instead of 12:40 — called me in a panic.
The fix
-
cooperative-stickyassignor — only moves the partitions that need to move (others keep processing) -
Increase
max.poll.interval.ms— gives consumers time to finish long batches before being kicked out -
Use static membership (
group.instance.id) — same pod keeps same partitions on rolling restart -
Decrease
session.timeout.ms— so dead consumers are detected fast
spring:
kafka:
consumer:
properties:
partition.assignment.strategy: CooperativeStickyAssignor
group.instance.id: ${HOSTNAME}
session.timeout.ms: 10000
max.poll.interval.ms: 300000
max.poll.records: 500
The cost if you get it wrong
You add capacity at noon every day (peak hours). Every rebalance costs you 40 seconds. Multiplied by all the autoscaling you do in a day, 20 minutes of daily lag = owner loses trust in your dashboard = your team gets pulled off the next sprint to "make the numbers stable."
7. "Explain zero-copy. Why does Kafka use it?"
TL;DR: The kernel copies the file directly to the socket buffer, bypassing user-space. CPU doesn't touch the data.
PSI scenario: serving product images to a 4G phone in Zambia
psi-goods serves 50,000 product images. Average size 200KB. The retailer in a Lusaka township is on 4G with 2 Mbps down. If we go through user-space (read → compress → send), each image takes 800ms. With zero-copy (sendfile), 200ms. Same bandwidth, 4x faster, no Java heap pressure.
Why Kafka uses it for log shipping
When a consumer fetches from the broker:
- Old way: disk → kernel buffer → user buffer → kernel socket buffer → NIC
- Zero-copy: disk → kernel buffer → NIC (kernel uses
sendfile(2)syscall)
The broker CPU stays idle. The same 32-core broker that handled 200 MB/s with old path handles 2 GB/s with zero-copy.
The cost if you get it wrong
You serve product images through your app. JVM heap grows. GC happens every 30 seconds. The 4G retailer waits 8 seconds per image swipe. They close the app. They buy from your competitor who has zero-copy.
8. "How do you prevent oversell with Redis?"
TL;DR: SET key value NX EX seconds for the lock + a Lua script for atomic check-and-set. Never trust SETNX alone — no TTL, no atomicity.
PSI scenario: the most-feared bug in retail
Customer scans a Coca-Cola. psi-cashier does:
-
GET stock:coke→ returns5 - Customer pays
-
SET stock:coke 4→ commit
What if 200 customers scan simultaneously and GET all return 5? You sell 200 bottles with 5 in stock. Cash register doesn't stop. You're out $400.
The real implementation
// Lua script = atomic check-and-decrement
private static final String DECREASE_STOCK =
"local stock = tonumber(redis.call('get', KEYS[1])) " +
"if stock == nil or stock <= 0 then return -1 end " +
"redis.call('decr', KEYS[1]) " +
"return stock - 1";
public boolean tryDeductStock(String sku, int qty) {
for (int i = 0; i < 3; i++) { // retry on lock contention
Long remaining = (Long) redis.eval(
DECREASE_STOCK, 1, "stock:" + sku
);
if (remaining != null && remaining >= 0) return true;
if (remaining != null && remaining == -1) return false; // out of stock
Thread.sleep(20);
}
return false;
}
The Lua script runs atomically inside Redis (single-thread model = no race). If stock goes negative, return -1. Client decides whether to retry.
The cost if you get it wrong
100 bottles sold, 150 deducted from inventory. $200 lost per incident. Multiply by a weekend: $1,200 + 4 hours of customer service calls. Now your boss thinks your system is "unstable."
9. "What if your Kafka consumers can't keep up?"
TL;DR: Add partitions, add consumers. Both are needed. Then increase batch size. Then add monitoring so you know before lag becomes a problem.
PSI scenario: PSI Black Friday promotion
Last Black Friday: 10x normal volume. Lag hit 45 minutes within 2 hours. Owner called at 9 PM saying "the dashboard says we made $0 today."
The 4-step drill
- Increase partitions (off-peak, requires rebalance)
- Increase consumer instances to match new partition count
-
Increase
max.poll.recordsfrom 500 to 2000 -
Add a lag dashboard —
kafka-consumer-groups.sh --describeevery 30 seconds
spring:
kafka:
listener:
concurrency: 12 # match partition count
type: batch
consumer:
max:
poll:
records: 2000
The cost if you get it wrong
Lag = lost trust. Owner calls. You wake up at 1 AM to scale. The customer who saw the lag never comes back. You fix it for Black Friday, but the next promo will have the same problem unless the monitoring tells you in advance.
10. "Walk me through the 8 Redis eviction policies."
TL;DR: They're knobs, not choices. Default is noeviction (writes fail when full). Pick one based on access pattern.
PSI scenario: 4GB old POS machine
We deploy PSI to retail stores with hand-me-down 4GB desktops. Redis is capped at 1.5GB (the rest is OS + app). With noeviction, the first hot day fills memory → writes fail → cash register can't record sales.
The choice
| Policy | Use when |
|---|---|
noeviction (default) |
Cache layer where DB is the truth — let writes fail loudly |
allkeys-lru |
Cache layer where losing is acceptable — most flexible |
volatile-lru |
Mix of cache + persistent data — only evict keys with TTL |
allkeys-lfu (Redis 4.0+) |
Hot data skewed — LFU is smarter than LRU on long-tail access |
volatile-random |
Rare; only when you don't know what's hot |
allkeys-random |
Almost never |
volatile-ttl |
When you explicitly tag disposable keys |
volatile-xxx (lfu, random, ttl) |
Same as volatile-lru but different algorithm |
We use allkeys-lru on cache services and noeviction on the lock-service (locks must not vanish).
The cost if you get it wrong
Default noeviction on cache → cash register dies at 3 PM every hot day → customer abandons cart → you find out at end of month when sales are 20% lower than forecast.
Closing: how to actually answer these in interviews
I've been through 14 backend interviews in the last year (mostly remote EU roles, mostly senior/lead level). The candidates who get the offer aren't the ones who recite text. They're the ones who:
- Open with the scenario, not the textbook: "In my POS system, this shows up when X happens..." — then explain.
- Always add the cost: "If you don't do this, here's what broke in my system."
- Name the trade-off they didn't take: Kafka vs RabbitMQ, Redis vs Caffeine, optimistic vs pessimistic lock — name what you didn't choose.
- Mention what you'd change next time: every senior knows their decisions were 80% right, not 100%.
If you're interviewing for a Java/Go backend role in 2026, the above 10 will cover ~70% of the system-design depth round. The other 30% is database indexing, async patterns, and observability — different article, similar format.
Good luck out there. The Redis book is not required reading.
About the author: Yuping Mo is a Java/Spring Boot architect with 13 years of experience. He currently builds inventory and POS systems for small businesses across Africa, the UK, and China, while interviewing for distributed-systems roles in Europe.
GitHub: @moyuping-java-architect
PSI project: inventory-pos-microsystem
Email: yuping.mo@outlook.com
Top comments (0)