DEV Community

moyuping
moyuping

Posted on

The Redis & Kafka Interview Questions I Got Asked 23 Times (And How I Finally Answered Them)

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 * / SMEMBERS on 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));
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

  1. cooperative-sticky assignor — only moves the partitions that need to move (others keep processing)
  2. Increase max.poll.interval.ms — gives consumers time to finish long batches before being kicked out
  3. Use static membership (group.instance.id) — same pod keeps same partitions on rolling restart
  4. 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. GET stock:coke → returns 5
  2. Customer pays
  3. 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;
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Increase partitions (off-peak, requires rebalance)
  2. Increase consumer instances to match new partition count
  3. Increase max.poll.records from 500 to 2000
  4. Add a lag dashboardkafka-consumer-groups.sh --describe every 30 seconds
spring:
  kafka:
    listener:
      concurrency: 12  # match partition count
      type: batch
    consumer:
      max:
        poll:
          records: 2000
Enter fullscreen mode Exit fullscreen mode

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:

  1. Open with the scenario, not the textbook: "In my POS system, this shows up when X happens..." — then explain.
  2. Always add the cost: "If you don't do this, here's what broke in my system."
  3. Name the trade-off they didn't take: Kafka vs RabbitMQ, Redis vs Caffeine, optimistic vs pessimistic lock — name what you didn't choose.
  4. 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)