<?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%2F0026d1ed-a779-439c-a109-9881b851a337.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>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>
