惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
CERT Recently Published Vulnerability Notes
U
Unit 42
T
The Blog of Author Tim Ferriss
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Securelist
L
Lohrmann on Cybersecurity
Blog — PlanetScale
Blog — PlanetScale
Recorded Future
Recorded Future
D
DataBreaches.Net
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
I
Intezer
P
Palo Alto Networks Blog
Simon Willison's Weblog
Simon Willison's Weblog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
I
InfoQ
宝玉的分享
宝玉的分享
Security Latest
Security Latest
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Threatpost
Cisco Talos Blog
Cisco Talos Blog
P
Proofpoint News Feed
博客园 - 司徒正美
H
Hacker News: Front Page
Y
Y Combinator Blog
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
NISL@THU
NISL@THU
月光博客
月光博客
有赞技术团队
有赞技术团队
Cloudbric
Cloudbric
酷 壳 – CoolShell
酷 壳 – CoolShell
G
Google Developers Blog
A
Arctic Wolf
博客园 - 【当耐特】
W
WeLiveSecurity
V
Visual Studio Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
V
V2EX
C
Cyber Attacks, Cyber Crime and Cyber Security
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
The Cloudflare Blog
Stack Overflow Blog
Stack Overflow Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Your Inventory Counter Just Went Negative. Here’s Why — and How to Fix It.
Archit Agarw · 2026-05-18 · via DEV Community

Black Friday. 9:02am. Your e-commerce platform has been live for two minutes.

A limited-edition sneaker drops - 500 pairs in stock. Within seconds, the inventory counter is spinning. Add to cart. Checkout. Payment confirmed. Your servers in the US, EU, and Asia are all taking orders simultaneously to keep latency low for every customer on the planet.

By 9:04am, you've sold 620 pairs.

You have 500.

Your ops channel lights up. Support tickets flood in. You're now emailing 120 paying customers to tell them their confirmed order doesn't exist. The refunds go out. The trust doesn't come back.

What went wrong wasn't a bug in your payment code. It wasn't a race condition in the traditional sense. It was something more fundamental: your distributed counter had no floor. And when multiple servers decremented it concurrently, each one saw a local view that looked safe - and the global invariant silently broke.

This is the problem Bounded Counters were designed to solve. And the solution is one of the most elegant ideas in the CRDT literature.

What an Inventory Counter Actually Is

Before the distributed systems, let's ground this in something simple.

An inventory counter is just a number. It represents how many units of something you have available. When a customer buys one, the number goes down. When a shipment arrives, the number goes up. The rule is straightforward:

The counter must never go below zero.

In SQL, this is trivial:

CREATE TABLE inventory (
    product_id  BIGINT PRIMARY KEY,
    stock       INT DEFAULT 0 CHECK (stock >= 0)
);

Enter fullscreen mode Exit fullscreen mode

The CHECK constraint enforces the floor. If a transaction tries to bring stock below zero, the database rejects it. One server. One source of truth. The constraint holds perfectly.

The same logic applies to a wallet: balance must never go below zero. To an ad budget: spend must never exceed the limit. To a rate limiter: requests must never exceed the ceiling.

These are bounded counters - counters with a numeric invariant that must never be violated.

Simple when you have one server. Deeply interesting when you have many.

The Moment Simplicity Breaks

Your platform grows. One server can't handle Black Friday traffic. You distribute.

You deploy database replicas in the US, EU, and Asia. Each region accepts writes locally to keep latency low. The system is eventually consistent - replicas sync in the background, but any given server might be working from a slightly stale view of the world.

Here's what happens to your inventory counter.

500 pairs in stock. Three servers, each seeing the same initial state: stock = 500. In the first two minutes, customers are hammering all three regions simultaneously.

The US server processes 200 orders. Locally: 500 - 200 = 300. Looks fine.

The EU server, working from the same initial state it loaded before the US orders propagated, processes 180 orders. Locally: 500 - 180 = 320. Also looks fine.

The Asia server processes 170 orders. Locally: 500 - 170 = 330. Still fine locally.

When the three servers finally sync: 500 - 200 - 180 - 170 = -50.
You've oversold by 50 pairs. Every check passed locally. The global invariant shattered silently.

Why the Solutions You're Thinking Of Don't Work Here

At this point, two approaches come to mind. Both fail at scale in ways worth understanding.

Distributed locks: Before decrementing, acquire a global lock on the inventory key across all replicas. Only one server writes at a time.

This works - and kills your latency. A lock that spans data centres separated by hundreds of milliseconds of network round-trip adds that latency to every single purchase. On Black Friday, when the whole point of distributing was to make checkout fast, a global lock turns your purchase flow into a queue. Users abandon cart. Revenue drops.

Strong consistency everywhere: Route all inventory writes through a single primary node or quorum. Every decrement gets serialised.

Also works. Also slow. Same problem as distributed locks, different mechanism. The CAP theorem is not negotiable: if you want strong consistency across geographically distributed nodes, you pay in latency and availability.

In Episode 2, PN-Counter gave us increment and decrement without coordination. But I ended that piece with a warning: PN-Counter has no floor. Nothing in its design prevents the score from going to -50,000. For Reddit karma, nobody cares. For inventory, that's a P0 incident.

We need something new.

The Insight: Stop Distributing the Counter. Distribute the Permission.

Here's the shift in thinking that makes Bounded Counters work.

With PN-Counter, we stopped trying to coordinate on every write. We accepted that replicas might temporarily disagree on the exact value. That was fine because eventual consistency was acceptable.

Bounded Counters need something stronger: a hard floor that can never be violated, even under concurrent writes, even during network partitions, even when replicas have stale views.

The key insight: you don't need every replica to know the exact current value to enforce the floor. You just need every replica to know it has permission to decrement.

Think of it like tickets.

You have 100 items in stock. The floor is 0. That means you have 100 units of "decrement permission" - 100 tickets. You distribute those tickets across your three servers before the sale starts:

  • US server: 50 tickets
  • EU server: 30 tickets
  • Asia server: 20 tickets

Now the rule is simple: a server can only decrement if it has at least one ticket to spend. When it decrements by N, it spends N tickets.

The US server takes 50 orders: spends 50 tickets, now has 0. Stops accepting orders locally. The EU server takes 30 orders: spends 30 tickets, now has 0. Stops accepting orders locally. The Asia server takes 20 orders: spends 20 tickets, now has 0. Stops.

Total orders: 100. Exactly the stock. Global invariant: never violated.

Nobody coordinated on a single order. Every check was purely local. The constraint held globally because the math is airtight: if total tickets = total available decrements, and each replica can only spend what it owns, the sum of decrements can never exceed the total tickets.

This is the escrow idea, applied to distributed systems. You're not distributing the counter. You're distributing the permission to decrement.

The Wallet Walk-Through

Let's trace this concretely with a wallet that must never go below zero.

Initial state: Wallet value = 100. Floor = 0. Available decrement rights = 100 − 0 = 100.

Rights distributed:

DC_A: 50 rights
DC_B: 30 rights
DC_C: 20 rights

Enter fullscreen mode Exit fullscreen mode

Payment at DC_A: User pays 15 units.

DC_A checks local rights: 50 ≥ 15 ✓. Executes locally. No coordination needed.

DC_A: 35 rights remaining
DC_B: 30 rights remaining
DC_C: 20 rights remaining
Global value: 85

Enter fullscreen mode Exit fullscreen mode

Concurrent payment at DC_B: User pays 25 units.

DC_B checks local rights: 30 ≥ 25 ✓. Executes locally. DC_A and DC_B never talked during either payment.

DC_A: 35 rights remaining
DC_B: 5 rights remaining
DC_C: 20 rights remaining
Global value: 60

Enter fullscreen mode Exit fullscreen mode

Both payments went through. Both were validated purely locally. The global floor - wallet ≥ 0 - was never at risk, because the sum of all rights (35 + 5 + 20 = 60) exactly equals the remaining balance.

Failed payment at DC_C: User tries to pay 25 units.

DC_C checks local rights: 20 < 25 ✗. Cannot guarantee the floor locally.

Two options: fail immediately, or coordinate to borrow rights from another replica.

Rights transfer: DC_C asks DC_A for 10 rights. DC_A has 35 and can spare them.

DC_A: 25 rights  (gave 10 to DC_C)
DC_B: 5 rights
DC_C: 30 rights  (received 10 from DC_A)

Enter fullscreen mode Exit fullscreen mode

DC_C re-attempts: 30 ≥ 25 ✓. Payment goes through.

DC_A: 25 rights
DC_B: 5 rights
DC_C: 5 rights
Global value: 35

Enter fullscreen mode Exit fullscreen mode

At no point did the wallet go below zero. At no point did all three servers need to agree on the exact current value before processing a payment. The invariant held through local checks alone - with one brief coordination event only when a replica ran out of its allocated rights.

The Data Structure

The Bounded Counter state at each replica tracks three things:

type BoundedCounter struct {
    Min int64     // lower bound (e.g. 0)
    N   int       // number of replicas
    ID  int       // this replica's index

    R [][]int64   // R[i][j]: rights replica i transferred to j
                  // R[i][i]: rights replica i created for itself (via increments)
    U []int64     // U[i]: decrements executed at replica i
}

Enter fullscreen mode Exit fullscreen mode

The rights matrix R is the core. Each replica owns the diagonal - R[i][i] is the rights it created. Off-diagonal entries R[i][j] track rights transferred from replica i to replica j. Every entry only ever increases. This is the grow-only constraint, applied to a matrix instead of a single value.

Reading the current value:

func (c *BoundedCounter) Value() int64 {
    var inc, dec int64
    for i := 0; i < c.N; i++ {
        inc += c.R[i][i]
        dec += c.U[i]
    }
    return c.Min + inc - dec
}

Enter fullscreen mode Exit fullscreen mode

Sum the diagonal (total increments), subtract total decrements, add the floor.

Checking local rights:

func (c *BoundedCounter) LocalRights() int64 {
    id := c.ID
    var created, received, sent, spent int64
    created = c.R[id][id]
    for i := 0; i < c.N; i++ {
        if i == id { continue }
        received += c.R[i][id]
        sent += c.R[id][i]
    }
    spent = c.U[id]
    return created + received - sent - spent
}

Enter fullscreen mode Exit fullscreen mode

Rights I created, plus rights I received, minus rights I gave away, minus rights I already spent.

Decrementing:

func (c *BoundedCounter) Dec(n int64) error {
    if c.LocalRights() < n {
        return fmt.Errorf("not enough rights")
    }
    c.U[c.ID] += n
    return nil
}

Enter fullscreen mode Exit fullscreen mode

Only decrement if you have the tickets. Otherwise, reject or trigger a rights transfer.

Transferring rights:

func (c *BoundedCounter) Transfer(to int, n int64) error {
    if c.LocalRights() < n {
        return fmt.Errorf("not enough rights to transfer")
    }
    c.R[c.ID][to] += n
    return nil
}

Enter fullscreen mode Exit fullscreen mode

The merge - identical pattern to every CRDT we've built:

func (c *BoundedCounter) Merge(other *BoundedCounter) error {
    for i := 0; i < c.N; i++ {
        for j := 0; j < c.N; j++ {
            if other.R[i][j] > c.R[i][j] {
                c.R[i][j] = other.R[i][j]
            }
        }
        if other.U[i] > c.U[i] {
            c.U[i] = other.U[i]
        }
    }
    return nil
}

Enter fullscreen mode Exit fullscreen mode

Element-wise maximum. Every entry in R and U only ever grows. Merge is commutative, associative, idempotent. Replicas always converge.

Why the Invariant Can Never Break

Here's the mathematical guarantee, stated plainly.

Total decrement rights across all replicas = current value − floor.

Each replica can only decrement by spending rights it owns. A replica cannot own negative rights. Therefore, the total decrements across all replicas can never exceed the total rights in the system. Therefore, the global value can never drop below the floor.

This holds even when replicas have stale views of each other's state. Each replica's local check - "do I have enough rights?" - is conservative by design. If you have 20 rights and the operation needs 25, you say no. You might technically have been able to borrow rights from another replica - but you don't know that yet, and the safe answer is always no.

Coordination is infrequent and amortised. For operations that fit within local rights, zero coordination. For operations that exceed local rights, one round-trip to request a transfer. Compared to coordinating on every write, this is a dramatic improvement in practice.

The Trade-Off You Should Know

Bounded Counter is the most complex CRDT in this series so far - and it comes with trade-offs worth naming.

Rights starvation: if a replica runs out of rights and the replica it needs to borrow from is unreachable (network partition, server failure), the operation fails. The invariant is safe - you'd rather fail an operation than violate the floor - but the user experience is a rejection, not a delay. For inventory, that means "out of stock" even when stock technically exists elsewhere. Acceptable for correctness. Worth designing around.

State size scales with replica count: the R matrix is N×N. With 3 replicas, 9 cells. With 100 replicas, 10,000. For most geo-distributed systems (2-7 data centres), this is negligible. For edge computing with thousands of nodes, you'd want a different approach.

Rights rebalancing is a background concern: a replica that consistently gets more traffic than its rights allocation will hit starvation more often. Operational tooling to monitor and rebalance rights distribution is not optional - it's part of running this in production.

The Bigger Pattern

Three episodes in, the shape of CRDT design is clear.

Every CRDT we've built solves a hard distributed problem by identifying a constraint that eliminates ambiguity.

G-Counter said: make it grow-only. The maximum is always correct.

PN-Counter said: never decrement - use two grow-only counters and subtract at read time.

Bounded Counter says: never coordinate on every operation - distribute the permission to operate instead.

The constraint is always the solution. The art is finding the right one.

What's Next

The next piece steps back from counters entirely.

What if instead of tracking numbers, you need to track membership - items in a set, users in a group, tags on a document? The same concurrent update problems apply. And a surprisingly simple grow-only constraint solves the basic case cleanly.

Episode 4: G-Set - grow-only sets, where the items go in but never come out, and why that constraint enables a clean distributed implementation.

The full series:

Subscribe to the newsletter to get each episode as it drops.
If you're building distributed systems and want to think through invariant enforcement, CRDT design, or distributed architecture with someone who has been deep in this space - I do 1:1 sessions on Topmate.