DEV Community

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 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 refundProducerFactory() { return new DefaultKafkaProducerFactory<>(props, ..., new JsonSerializer ()) {{ 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-sticky assignor - 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) 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.records from 500 to 2000 - Add a lag dashboard - kafka-consumer-groups.sh --describe every 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 come

Comments

No comments yet. Start the discussion.