<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: moyuping</title>
    <description>The latest articles on DEV Community by moyuping (@moyuping).</description>
    <link>https://dev.to/moyuping</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4055920%2F3c6d8cf3-108c-406d-a2a9-64cd2c3235de.png</url>
      <title>DEV Community: moyuping</title>
      <link>https://dev.to/moyuping</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/moyuping"/>
    <language>en</language>
    <item>
      <title>The Redis &amp; Kafka Interview Questions I Got Asked 23 Times (And How I Finally Answered Them)</title>
      <dc:creator>moyuping</dc:creator>
      <pubDate>Thu, 06 Aug 2026 23:46:05 +0000</pubDate>
      <link>https://dev.to/moyuping/the-redis-kafka-interview-questions-i-got-asked-23-times-and-how-i-finally-answered-them-2ho</link>
      <guid>https://dev.to/moyuping/the-redis-kafka-interview-questions-i-got-asked-23-times-and-how-i-finally-answered-them-2ho</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  0. Context (read this once, skip on reread)
&lt;/h2&gt;

&lt;p&gt;These answers come from &lt;strong&gt;PSI&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;p&gt;If you only want the TL;DR per question, each section starts with one. If you want the war stories, keep reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. "Why is Redis so fast if it's single-threaded?"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Single-threading isn't the bottleneck — it's the &lt;em&gt;feature&lt;/em&gt;. One thread = no lock contention = O(1) for almost everything.&lt;/p&gt;

&lt;p&gt;The textbook answer is "memory + I/O multiplexing + single thread." True but useless in interviews. The real question is: &lt;strong&gt;what did you optimize away by going single-threaded?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You optimized away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lock contention on data structures (single thread, single owner)&lt;/li&gt;
&lt;li&gt;Context switches between threads (one less source of latency spikes)&lt;/li&gt;
&lt;li&gt;Cache line bouncing across cores (one CPU, one L1, no coherency traffic)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why &lt;code&gt;GET&lt;/code&gt;/&lt;code&gt;SET&lt;/code&gt; stay at sub-millisecond even under 100K QPS.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this looked like in PSI
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;psi-goods&lt;/code&gt;, 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, &lt;strong&gt;miss storms just queue up&lt;/strong&gt; — they don't deadlock. We ran 4KB average value, 2M keys on a 8GB instance, p99 &amp;lt; 4ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost if you get it wrong
&lt;/h3&gt;

&lt;p&gt;I picked Memcached once in 2018 because someone on a blog said "Memcached is faster on benchmarks." Six months later I wanted &lt;code&gt;SCAN&lt;/code&gt;, persistence, and Pub/Sub. Three months of migration back. The benchmarks were right; the blog forgot to mention &lt;em&gt;what&lt;/em&gt; Memcached can't do.&lt;/p&gt;

&lt;h3&gt;
  
  
  When interviewers actually care
&lt;/h3&gt;

&lt;p&gt;They're listening for whether you know &lt;strong&gt;when single-thread breaks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;KEYS *&lt;/code&gt; / &lt;code&gt;SMEMBERS&lt;/code&gt; on large collections (blocks the single thread)&lt;/li&gt;
&lt;li&gt;Big key &lt;code&gt;DEL&lt;/code&gt; (frees memory slowly, blocks)&lt;/li&gt;
&lt;li&gt;Long-running Lua scripts (blocks for the duration)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only know the textbook answer, you sound like you read it yesterday.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. "Walk me through the three cache problems."
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Penetration (null doesn't exist), Avalanche (thundering herd at expiry), Breakdown (hot key expires, dogpile on DB).&lt;/p&gt;

&lt;p&gt;Don't list them. Show me the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  PSI scenario: QR-code scan-to-order
&lt;/h3&gt;

&lt;p&gt;Each scan pings &lt;code&gt;psi-cashier&lt;/code&gt;, which checks SKU via &lt;code&gt;psi-goods&lt;/code&gt;. If the SKU isn't cached AND isn't in the DB, we still need a &lt;em&gt;fast&lt;/em&gt; answer ("not found") instead of letting 1,000 concurrent scans stampede the DB.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix — three lines, three problems
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Penetration — cache the "negative" too&lt;/span&gt;
&lt;span class="nc"&gt;Sku&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sku:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sku:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"NULL_TOKEN"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 5-min null cache&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sku:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;JsonUtil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Breakdown — single-flight via distributed lock&lt;/span&gt;
&lt;span class="nc"&gt;Sku&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sku:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;lockKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"lock:sku:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lockKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"NX"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"EX"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// got the lock&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Sku&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sku:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;JsonUtil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;del&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lockKey&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(...);&lt;/span&gt; &lt;span class="c1"&gt;// wait + retry&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Avalanche — random jitter on TTL&lt;/span&gt;
&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;baseTtl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalRandom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;nextLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sku:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseTtl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;jitter&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;JsonUtil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The cost if you get it wrong
&lt;/h3&gt;

&lt;p&gt;In 2024, a Black Friday promotion hit &lt;code&gt;psi-goods&lt;/code&gt; 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. &lt;strong&gt;Stores stopped scanning for a minute and a half&lt;/strong&gt;. Customers walked out. Owner called me at 11 PM. That's the cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. "When would you &lt;em&gt;not&lt;/em&gt; use Redis?"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: When the data is local-only AND changes rarely AND every node needs to see it eventually consistent.&lt;/p&gt;

&lt;p&gt;This is the question junior candidates fail. They answer "use Redis everywhere."&lt;/p&gt;

&lt;h3&gt;
  
  
  PSI scenario: permission tree
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;psi-system&lt;/code&gt;, every API checks the caller's role against the menu/permission tree. The tree has 200 nodes. It changes maybe once a week. &lt;strong&gt;Redis is overkill&lt;/strong&gt; — the round-trip to Redis is 1ms; the code execution is 0.1ms; the network variance adds 5ms p99 spikes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Caffeine local cache + Redis pub/sub invalidation&lt;/span&gt;
&lt;span class="nc"&gt;LoadingCache&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PermissionTree&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;localCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Caffeine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maximumSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expireAfterWrite&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MINUTES&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"perm:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// One node updates → publish&lt;/span&gt;
&lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;convertAndSend&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"perm-invalidate"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"all"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Every node listens&lt;/span&gt;
&lt;span class="nd"&gt;@EventListener&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onInvalidate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;localCache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invalidateAll&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: 4-microsecond reads on the hot path, 5-minute consistency window across 30 stores. &lt;strong&gt;The cost of inconsistency is zero&lt;/strong&gt; because the permission tree almost never changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost if you get it wrong
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. "Why Kafka and not RabbitMQ?"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Because I need to replay the past. RabbitMQ deletes after ack; Kafka keeps the log.&lt;/p&gt;

&lt;p&gt;Interviewers ask this to test if you understand &lt;strong&gt;the model difference&lt;/strong&gt;, not "which is faster."&lt;/p&gt;

&lt;h3&gt;
  
  
  PSI scenario: daily close audit
&lt;/h3&gt;

&lt;p&gt;Every night, &lt;code&gt;psi-finance&lt;/code&gt; 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 &lt;strong&gt;replay&lt;/strong&gt; those 30 days of events.&lt;/p&gt;

&lt;p&gt;With Kafka, I just reset the consumer offset to 30 days ago and re-run. With RabbitMQ, those events are gone.&lt;/p&gt;

&lt;h3&gt;
  
  
  When RabbitMQ is better
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Single-shot task distribution ("send this email")&lt;/li&gt;
&lt;li&gt;Per-message routing that changes frequently&lt;/li&gt;
&lt;li&gt;Strict per-message ordering across &lt;em&gt;all&lt;/em&gt; consumers (RabbitMQ's queue model is naturally strict)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost if you get it wrong
&lt;/h3&gt;

&lt;p&gt;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.)&lt;/p&gt;




&lt;h2&gt;
  
  
  5. "How do you guarantee exactly-once?"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: You don't. You guarantee &lt;strong&gt;at-least-once + idempotent consumer&lt;/strong&gt;. Exactly-once is marketing.&lt;/p&gt;

&lt;p&gt;The "transactional producer + read-committed consumer" combo gets &lt;em&gt;effectively&lt;/em&gt; exactly-once, but the language is misleading.&lt;/p&gt;

&lt;h3&gt;
  
  
  PSI scenario: refund + inventory sync
&lt;/h3&gt;

&lt;p&gt;When a customer asks for a refund, &lt;code&gt;psi-finance&lt;/code&gt; issues the refund, then must restock the item in &lt;code&gt;psi-goods&lt;/code&gt;. If the refund is processed but inventory isn't restocked → money gone, stock gone. &lt;strong&gt;Worst case scenario&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Producer: idempotent (retries don't duplicate)&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ProducerFactory&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RefundEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;refundProducerFactory&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DefaultKafkaProducerFactory&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JsonSerializer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RefundEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;())&lt;/span&gt; &lt;span class="o"&gt;{{&lt;/span&gt;
        &lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"enable.idempotence"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// broker dedupes by producer-id + sequence&lt;/span&gt;
        &lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"acks"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"all"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"max.in.flight.requests.per.connection"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}};&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Consumer: idempotent via event_id check&lt;/span&gt;
&lt;span class="nd"&gt;@KafkaListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"refund-events"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onRefund&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RefundEvent&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;dedupKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"refund:processed:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eventId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setIfAbsent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dedupKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofDays&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;financeService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reversePayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="n"&gt;goodsService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;restock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// already processed — silently drop&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two layers: producer idempotence kills network retries, consumer idempotence kills redelivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost if you get it wrong
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. "What happens when a Kafka consumer group rebalances?"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Everything stops. 30 seconds to a few minutes if you're not careful.&lt;/p&gt;

&lt;h3&gt;
  
  
  PSI scenario: psi-report sales dashboard
&lt;/h3&gt;

&lt;p&gt;The sales report consumer reads from 3 topics (&lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;payments&lt;/code&gt;, &lt;code&gt;refunds&lt;/code&gt;) and writes a per-store daily rollup to Postgres. It runs on 6 consumer instances for parallelism. When we &lt;em&gt;added&lt;/em&gt; a 7th instance at noon, &lt;strong&gt;all 6 existing consumers paused&lt;/strong&gt;, partitions shuffled, processing resumed.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cooperative-sticky&lt;/code&gt; assignor&lt;/strong&gt; — only moves the partitions that need to move (others keep processing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Increase &lt;code&gt;max.poll.interval.ms&lt;/code&gt;&lt;/strong&gt; — gives consumers time to finish long batches before being kicked out&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use static membership&lt;/strong&gt; (&lt;code&gt;group.instance.id&lt;/code&gt;) — same pod keeps same partitions on rolling restart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decrease &lt;code&gt;session.timeout.ms&lt;/code&gt;&lt;/strong&gt; — so dead consumers are detected fast
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kafka&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;consumer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;partition.assignment.strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CooperativeStickyAssignor&lt;/span&gt;
        &lt;span class="na"&gt;group.instance.id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${HOSTNAME}&lt;/span&gt;
        &lt;span class="na"&gt;session.timeout.ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;
        &lt;span class="na"&gt;max.poll.interval.ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300000&lt;/span&gt;
        &lt;span class="na"&gt;max.poll.records&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The cost if you get it wrong
&lt;/h3&gt;

&lt;p&gt;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, &lt;strong&gt;20 minutes of daily lag = owner loses trust in your dashboard = your team gets pulled off the next sprint to "make the numbers stable."&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. "Explain zero-copy. Why does Kafka use it?"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: The kernel copies the file directly to the socket buffer, bypassing user-space. CPU doesn't touch the data.&lt;/p&gt;

&lt;h3&gt;
  
  
  PSI scenario: serving product images to a 4G phone in Zambia
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;psi-goods&lt;/code&gt; 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 (&lt;code&gt;sendfile&lt;/code&gt;), 200ms. &lt;strong&gt;Same bandwidth, 4x faster&lt;/strong&gt;, no Java heap pressure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Kafka uses it for log shipping
&lt;/h3&gt;

&lt;p&gt;When a consumer fetches from the broker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Old way: disk → kernel buffer → user buffer → kernel socket buffer → NIC&lt;/li&gt;
&lt;li&gt;Zero-copy: disk → kernel buffer → NIC (kernel uses &lt;code&gt;sendfile(2)&lt;/code&gt; syscall)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost if you get it wrong
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. "How do you prevent oversell with Redis?"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: &lt;code&gt;SET key value NX EX seconds&lt;/code&gt; for the lock + a Lua script for atomic check-and-set. &lt;strong&gt;Never trust &lt;code&gt;SETNX&lt;/code&gt; alone&lt;/strong&gt; — no TTL, no atomicity.&lt;/p&gt;

&lt;h3&gt;
  
  
  PSI scenario: the most-feared bug in retail
&lt;/h3&gt;

&lt;p&gt;Customer scans a Coca-Cola. &lt;code&gt;psi-cashier&lt;/code&gt; does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;GET stock:coke&lt;/code&gt; → returns &lt;code&gt;5&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Customer pays&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SET stock:coke 4&lt;/code&gt; → commit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What if 200 customers scan simultaneously and &lt;code&gt;GET&lt;/code&gt; all return &lt;code&gt;5&lt;/code&gt;? &lt;strong&gt;You sell 200 bottles with 5 in stock.&lt;/strong&gt; Cash register doesn't stop. You're out $400.&lt;/p&gt;

&lt;h3&gt;
  
  
  The real implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Lua script = atomic check-and-decrement&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;DECREASE_STOCK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="s"&gt;"local stock = tonumber(redis.call('get', KEYS[1])) "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;"if stock == nil or stock &amp;lt;= 0 then return -1 end "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;"redis.call('decr', KEYS[1]) "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;"return stock - 1"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;tryDeductStock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// retry on lock contention&lt;/span&gt;
        &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eval&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="no"&gt;DECREASE_STOCK&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"stock:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// out of stock&lt;/span&gt;
        &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Lua script runs atomically inside Redis (single-thread model = no race). If stock goes negative, return -1. Client decides whether to retry.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost if you get it wrong
&lt;/h3&gt;

&lt;p&gt;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."&lt;/p&gt;




&lt;h2&gt;
  
  
  9. "What if your Kafka consumers can't keep up?"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Add partitions, add consumers. Both are needed. Then increase batch size. Then add monitoring so you know &lt;em&gt;before&lt;/em&gt; lag becomes a problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  PSI scenario: PSI Black Friday promotion
&lt;/h3&gt;

&lt;p&gt;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."&lt;/p&gt;

&lt;h3&gt;
  
  
  The 4-step drill
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Increase partitions&lt;/strong&gt; (off-peak, requires rebalance)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Increase consumer instances&lt;/strong&gt; to match new partition count&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Increase &lt;code&gt;max.poll.records&lt;/code&gt;&lt;/strong&gt; from 500 to 2000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a lag dashboard&lt;/strong&gt; — &lt;code&gt;kafka-consumer-groups.sh --describe&lt;/code&gt; every 30 seconds
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kafka&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;listener&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;  &lt;span class="c1"&gt;# match partition count&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch&lt;/span&gt;
    &lt;span class="na"&gt;consumer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;poll&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;records&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The cost if you get it wrong
&lt;/h3&gt;

&lt;p&gt;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 &lt;strong&gt;the next promo will have the same problem unless the monitoring tells you in advance.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  10. "Walk me through the 8 Redis eviction policies."
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: They're knobs, not choices. Default is &lt;code&gt;noeviction&lt;/code&gt; (writes fail when full). Pick one based on access pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  PSI scenario: 4GB old POS machine
&lt;/h3&gt;

&lt;p&gt;We deploy PSI to retail stores with hand-me-down 4GB desktops. Redis is capped at 1.5GB (the rest is OS + app). With &lt;code&gt;noeviction&lt;/code&gt;, the first hot day fills memory → writes fail → cash register can't record sales.&lt;/p&gt;

&lt;h3&gt;
  
  
  The choice
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;noeviction&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;Cache layer where DB is the truth — let writes fail loudly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allkeys-lru&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cache layer where losing is acceptable — most flexible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;volatile-lru&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mix of cache + persistent data — only evict keys with TTL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;allkeys-lfu&lt;/code&gt; (Redis 4.0+)&lt;/td&gt;
&lt;td&gt;Hot data skewed — LFU is smarter than LRU on long-tail access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;volatile-random&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rare; only when you don't know what's hot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allkeys-random&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Almost never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;volatile-ttl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When you explicitly tag disposable keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;volatile-xxx (lfu, random, ttl)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as volatile-lru but different algorithm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We use &lt;strong&gt;&lt;code&gt;allkeys-lru&lt;/code&gt; on cache services&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;noeviction&lt;/code&gt; on the lock-service&lt;/strong&gt; (locks must not vanish).&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost if you get it wrong
&lt;/h3&gt;

&lt;p&gt;Default &lt;code&gt;noeviction&lt;/code&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing: how to actually answer these in interviews
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open with the scenario, not the textbook&lt;/strong&gt;: "In my POS system, this shows up when X happens..." — then explain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always add the cost&lt;/strong&gt;: "If you don't do this, here's what broke in my system."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name the trade-off they didn't take&lt;/strong&gt;: Kafka vs RabbitMQ, Redis vs Caffeine, optimistic vs pessimistic lock — name what you &lt;em&gt;didn't&lt;/em&gt; choose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mention what you'd change next time&lt;/strong&gt;: every senior knows their decisions were 80% right, not 100%.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Good luck out there. The Redis book is not required reading.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;About the author&lt;/strong&gt;: 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.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/moyuping-java-architect" rel="noopener noreferrer"&gt;@moyuping-java-architect&lt;/a&gt;&lt;br&gt;
PSI project: &lt;a href="https://github.com/moyuping-java-architect/inventory-pos-microsystem" rel="noopener noreferrer"&gt;inventory-pos-microsystem&lt;/a&gt;&lt;br&gt;
Email: &lt;a href="mailto:yuping.mo@outlook.com"&gt;yuping.mo@outlook.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>redis</category>
      <category>kafka</category>
      <category>java</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Building a Zero-Loss Inventory Engine: Java + Go Hybrid Microservices in Production</title>
      <dc:creator>moyuping</dc:creator>
      <pubDate>Fri, 31 Jul 2026 04:30:47 +0000</pubDate>
      <link>https://dev.to/moyuping/building-a-zero-loss-inventory-engine-java-go-hybrid-microservices-in-production-27pm</link>
      <guid>https://dev.to/moyuping/building-a-zero-loss-inventory-engine-java-go-hybrid-microservices-in-production-27pm</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;When you're building a supply-chain SaaS that handles procurement, sales, inventory, POS, and finance across 12+ microservices, inventory consistency isn't a nice-to-have — it's the difference between a working product and a lawsuit.&lt;/p&gt;

&lt;p&gt;The challenge: multiple concurrent requests trying to deduct the same stock item simultaneously. Traditional approaches (database pessimistic locks, simple Redis locks) either kill throughput or leave race condition gaps that cause overselling.&lt;/p&gt;

&lt;p&gt;I needed a system that could handle high-concurrency stock operations with &lt;strong&gt;zero data loss&lt;/strong&gt; — not "eventually consistent," not "mostly correct," but &lt;em&gt;zero&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: Four-Layer Concurrency Defense
&lt;/h2&gt;

&lt;p&gt;I designed a hybrid Java + Go approach where each layer catches what the previous one misses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request → [1] Redis Lua Pre-deduct → [2] DB CAS Optimistic Lock → [3] Go Mutex → [4] Auto Compensation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 1: Redis Lua Atomic Pre-deduction
&lt;/h3&gt;

&lt;p&gt;The first line of defense is a Go microservice that handles stock pre-deduction via Redis Lua scripts. Why Go? Because the inventory service needs to handle thousands of concurrent stock operations per second, and Go's goroutine model is ideal for this workload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Simplified: Redis Lua script for atomic stock deduction&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;deductScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;`
  local current = tonumber(redis.call('GET', KEYS[1]))
  if current == nil then return -1 end
  local qty = tonumber(ARGV[1])
  if current &amp;lt; qty then return 0 end
  redis.call('SET', KEYS[1], current - qty)
  return 1
`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: Lua scripts execute atomically in Redis. No other operation can interleave between the check and the deduction. This eliminates the race condition at the cache layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just use Redis and call it a day?&lt;/strong&gt; Because Redis is volatile. If Redis crashes between pre-deduction and the database commit, you've lost the deduction. You need the database as the source of truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Database CAS Optimistic Lock
&lt;/h3&gt;

&lt;p&gt;After Redis pre-deducts, the Java service commits to MySQL using Compare-And-Swap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;available_qty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;available_qty&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sku_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;skuId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;available_qty&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the version doesn't match (another transaction committed first), the update affects 0 rows and we retry. This is standard optimistic locking, but it's the critical second layer — if Redis crashes, the database still prevents overselling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Go Mutex for In-Process Serialization
&lt;/h3&gt;

&lt;p&gt;Within the Go service itself, a mutex ensures that concurrent requests for the &lt;em&gt;same SKU&lt;/em&gt; are serialized within a single process. This reduces the number of CAS retries at the database layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;StockLock&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt; &lt;span class="c"&gt;// key: skuID, value: *sync.Mutex&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;StockLock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadOrStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 4: Automatic Compensation
&lt;/h3&gt;

&lt;p&gt;If something fails after pre-deduction (e.g., order creation fails, payment fails), the system automatically compensates by reversing the stock deduction. This is powered by a Saga compensation pattern with a transactional outbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;deductStock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;skuId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Redis pre-deduct (via Go service)&lt;/span&gt;
    &lt;span class="n"&gt;redisInventoryService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;preDeduct&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. DB CAS update&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inventoryMapper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;casDeduct&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentVersion&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OptimisticLockException&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Record for compensation&lt;/span&gt;
    &lt;span class="n"&gt;outboxMessageRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;StockCompensationMessage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CompensationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REVERSE&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. afterCommit dispatch&lt;/span&gt;
    &lt;span class="n"&gt;transactionTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeAfterCommit&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; 
        &lt;span class="n"&gt;rabbitTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;convertAndSend&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stock.events"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the transaction rolls back, the outbox message is never dispatched — but neither is the DB deduction. If the transaction commits but downstream processing fails, the dead letter queue catches it and compensation kicks in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Java + Go Split?
&lt;/h2&gt;

&lt;p&gt;You might wonder: why not do this entirely in Java or entirely in Go?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go for the inventory service:&lt;/strong&gt; Go's goroutines handle 10K+ concurrent stock operations with minimal memory overhead. A single Go service can saturate Redis's throughput without being the bottleneck. The custom Nacos HTTP client in Go also means the service is self-contained — no JVM dependency for a performance-critical path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Java for business logic:&lt;/strong&gt; The rest of the platform (12 services, 648 APIs) runs on Spring Boot 3.2 with Java 21 virtual threads. Java's ecosystem — Spring Cloud, MyBatis-Plus, RabbitMQ integration — is unmatched for complex business logic. The Java services call the Go inventory service through Resilience4j circuit breaker (50% failure threshold, 2s slow-call detection), so if the Go service is degraded, the system fails gracefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature toggles for canary switching:&lt;/strong&gt; The architecture supports zero-downtime switching between the Go implementation and a Java fallback. In production, we can route traffic to either implementation without redeployment — invaluable for testing and rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;In production across African markets (Zambia), this architecture has handled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero data loss&lt;/strong&gt; under high-concurrency flash-sale scenarios&lt;/li&gt;
&lt;li&gt;Automatic recovery from Redis failures (DB CAS catches it)&lt;/li&gt;
&lt;li&gt;Graceful degradation via circuit breaker when the Go service is slow&lt;/li&gt;
&lt;li&gt;Full audit trail via custom distributed tracing (AspectJ LTW)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Defense in depth works.&lt;/strong&gt; No single mechanism is perfect, but four layers complement each other — Redis for speed, DB for durability, mutex for efficiency, compensation for safety.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Choose the right tool for the job.&lt;/strong&gt; Go for performance-critical paths, Java for complex business logic. Don't be religious about language choice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Feature toggles &amp;gt; redeployment.&lt;/strong&gt; Being able to switch between implementations without downtime is worth the extra abstraction cost.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transactional outbox is non-negotiable.&lt;/strong&gt; If you're doing event-driven architecture, the outbox pattern ensures your events and your database state are always consistent.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;The full source code is open source: &lt;a href="https://github.com/moyuping-java-architect/inventory-pos-microsystem" rel="noopener noreferrer"&gt;github.com/moyuping-java-architect/inventory-pos-microsystem&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm a software architect with 13+ years of experience, currently open to remote positions in Europe. Feel free to reach out at &lt;code&gt;yuping.mo@outlook.com&lt;/code&gt; or connect on &lt;a href="https://github.com/moyuping-java-architect" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What's your approach to inventory consistency?&lt;/strong&gt; Have you used a similar multi-layer defense, or do you prefer a different pattern? Let me know in the comments!&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>java</category>
      <category>go</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
