<?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: hardi raval</title>
    <description>The latest articles on DEV Community by hardi raval (@hardi_raval_f6b2364385bac).</description>
    <link>https://dev.to/hardi_raval_f6b2364385bac</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%2F2574936%2F5997426d-3590-4b9a-8000-7bc8d016c7d6.png</url>
      <title>DEV Community: hardi raval</title>
      <link>https://dev.to/hardi_raval_f6b2364385bac</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hardi_raval_f6b2364385bac"/>
    <language>en</language>
    <item>
      <title>The Carrier Says Delivered. The Customer Says It Never Arrived. Who is Right?</title>
      <dc:creator>hardi raval</dc:creator>
      <pubDate>Sun, 30 Aug 2026 16:37:37 +0000</pubDate>
      <link>https://dev.to/hardi_raval_f6b2364385bac/the-carrier-says-delivered-the-customer-says-it-never-arrived-who-is-right-2d94</link>
      <guid>https://dev.to/hardi_raval_f6b2364385bac/the-carrier-says-delivered-the-customer-says-it-never-arrived-who-is-right-2d94</guid>
      <description>&lt;p&gt;Originally published on &lt;a href="https://medium.com/@hardiravals90/the-carrier-says-delivered-the-customer-says-it-never-arrived-who-is-right-8de14c9bf416" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; - cross-posting here since I know a lot of you spend more time on dev.to. This is the fifth post in a series on modeling orders and inventory for ecommerce backends.&lt;/p&gt;

&lt;p&gt;A reader replied to the post on returns and partial fulfillment with a question about shipments that split and do not complete. Answering it properly turned up something worth being honest about upfront. There is not one failure mode hiding behind "the shipment did not work out". There are at least four, and they do not share a fix. This  post walks through one of them in full. The other three, I will name - because you will hit them eventually - but not solve here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick context if you are new here:&lt;/strong&gt; a line item can ship across more than one shipment, and shipments need their own status, separate from the line item. That much is standard by now. What is not standard is what happens when the status of a shipment itself becomes a matter of dispute, not a fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivered, According to Whom?
&lt;/h2&gt;

&lt;p&gt;A carrier scans a package as delivered. The customer says it never arrived. Both are true statements - about different things. The carrier is reporting a scan event. The customer is reporting their own experience. Nothing in a naive schema has a way to hold both of those at once, because a naive schema treats &lt;code&gt;delivered&lt;/code&gt; as a fact, not a claim.&lt;/p&gt;

&lt;p&gt;That distinction matters more than it sounds like it should. Trust the status from the carrier blindly, and "delivered" in your system quietly comes to mean "the scanner from the carrier said so". That is a weaker claim than what everyone reading that status assumes it means. Trust the customer blindly instead, and you are refunding orders that genuinely arrived, every time someone claims otherwise. &lt;/p&gt;

&lt;p&gt;The fix is not a new shipment status. A dispute is not a state the shipment moves through - it is a claim layered on top of whatever the shipment status already says.&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;delivery_disputes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;shipment_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;shipments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- 'open', 'resolved_delivered', 'resolved_not_delivered'&lt;/span&gt;
  &lt;span class="n"&gt;opened_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;resolved_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6jcupxn1503ypy0t8s8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6jcupxn1503ypy0t8s8.png" alt=" " width="799" height="411"&gt;&lt;/a&gt;&lt;br&gt;
While a dispute is open, the shipment status stays exactly what the carrier reported. Nothing about the underlying stock changed - the ledger entry for that sale was correct the moment it was made, based on the information available at the time. This is a different kind of problem than a warehouse simply not having the stock it claimed to have. Here, the ledger was right. What is uncertain is a fact about the physical world, not about the data.&lt;/p&gt;

&lt;p&gt;Resolution depends on what actually happened, which your system usually cannot determine on its own - GPS data, photos, or a carrier investigation resolve it, not a query. If the dispute resolves as &lt;code&gt;resolved_delivered&lt;/code&gt;, nothing further happens; the original transaction stands. If it resolves as &lt;code&gt;resolved_not_delivered&lt;/code&gt;, that is the moment for a real correction:&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;stock_ledger&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reference_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reference_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'delivery_dispute_confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'manual_adjustment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'carrier investigation confirmed package not delivered, reshipped from stock'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The delta is zero, on purpose. It is the same pattern as a non-restocking return elsewhere in this series. Nothing about available inventory changed, but the event is worth recording. "Why did we reship this order" is a question someone will eventually ask, and a silent edit to a payment record is not an answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three More Ways a Shipment Quietly Breaks Your Numbers
&lt;/h2&gt;

&lt;p&gt;Here is the honest part. Delivery disputes are not even the most common way this goes wrong - they are just the one most people recognize immediately, because everyone has been on one side of that argument as a customer. The other three are quieter, and each one needs a genuinely different fix, not a variation on this one. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One of them is about a package that goes missing after it genuinely left the warehouse&lt;/strong&gt; - real stock, gone somewhere between your dock and the door of the customer. The ledger entry for that sale was correct. The fix has nothing to do with the ledger at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One of them is about a warehouse floor that cannot find stock your system swears is there&lt;/strong&gt; - which means the number was wrong before this order ever came in, not after. That is a different class of problem than a lost package, and treating it the same way will leave your ledger quietly wrong in a way nobody catches for months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One of them is what happens when the floor has some of what was promised, but not all of it&lt;/strong&gt; - not a clean yes or no, the situation real warehouses hand you more often than either extreme.&lt;/p&gt;

&lt;p&gt;Each of these needs its own schema decision, its own ledger entry, and - for at least one of them - a genuine "watch this fail, then watch it get fixed" moment that no amount of reading substitutes for. That is not a line for this article. It is Lesson 4 of the course I am building on this topic, the same one this whole series has been building toward: the schema, the live build, and - for the trickiest of the three - a demo where the wrong version breaks on screen before the correct one holds.&lt;/p&gt;

&lt;p&gt;If you have read this far because you are staring at one of these three right now, the waitlist is the fastest way to find out when the rest is ready: join the waitlist - free, in the works now, you will be the first to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Leaves You
&lt;/h2&gt;

&lt;p&gt;A delivery dispute is a claim layered on a shipment, not a new status  the shipment moves through - the ledger only changes once the dispute actually resolves as undelivered, and even then, the entry exists to record what happened, not because the original transaction was wrong. That is the one failure mode this post solves completely.&lt;/p&gt;

&lt;p&gt;The other three are real, they are common, and they do not share this fix. If today is not the day you need them, this post still gave you the one that shows up the most: the argument between what a carrier scanned and what a customer experienced, and why your schema needs room for both to be true statements about different things.&lt;/p&gt;

&lt;p&gt;This connects to Lesson 4 in the course I am building on this topic. If you want to know when it is ready: &lt;a href="https://tally.so/r/9qPKJE" rel="noopener noreferrer"&gt;join the waitlist&lt;/a&gt; - free, in the works now, you will be the first to know.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A note on scope: like the earlier posts, this assumes delivery confirmation and dispute resolution flow through your own system in a  reasonably timely way. A carrier or fulfillment partner that reports delivery status with long delays, or not at all, turns this clean two-state resolution into an ongoing reconciliation problem - a different, harder topic than what is covered here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>systemdesign</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Why Do Ecommerce Backends Keep Overselling Stock?</title>
      <dc:creator>hardi raval</dc:creator>
      <pubDate>Wed, 26 Aug 2026 21:06:21 +0000</pubDate>
      <link>https://dev.to/hardi_raval_f6b2364385bac/why-do-ecommerce-backends-keep-overselling-stock-44bg</link>
      <guid>https://dev.to/hardi_raval_f6b2364385bac/why-do-ecommerce-backends-keep-overselling-stock-44bg</guid>
      <description>&lt;p&gt;Originally published on &lt;a href="https://medium.com/@hardiravals90/why-do-ecommerce-backends-keep-overselling-stock-78510b184e76" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; - cross-posting here since I know a lot of you spend more time on dev.to. I'm a backend software engineer with 13 years of experience, most of it spent building and fixing ecommerce systems. This is the first in a series on modeling orders and inventory in a way that actually holds up in production.&lt;/p&gt;

&lt;p&gt;This is aimed at backend engineers who've shipped a few CRUD APIs and are ready for the next layer - the tradeoffs that only show up under real concurrency and scale. If you're just starting out with databases, some of this will be a stretch; if you've already designed a production inventory system, most of this will be familiar (though I'd still love to hear how you did it differently).&lt;/p&gt;

&lt;p&gt;Somewhere in the history of almost every ecommerce system, there is a version of the same incident: a flash sale or limited drop where more units sell than actually exist in stock. Customer support ends up fielding angry emails, and the postmortem traces back to the same root cause - an &lt;code&gt;inventory&lt;/code&gt; table with a single &lt;code&gt;quantity&lt;/code&gt; column, where under load, dozens of checkout requests read the same stale count, decrement it, and write it back. A classic lost-update race condition. No amount of "just add more servers" fixes a data model that is wrong at its core.&lt;/p&gt;

&lt;p&gt;I have seen a version of this play out at nearly every ecommerce company I have worked with. Swap in your own war story and the shape is almost always the same: most overselling, refund chaos, and 2 AM pages do not come from bad code - they come from a data model that was never designed to survive concurrency, returns, or scale.&lt;/p&gt;

&lt;p&gt;This article walks through the decisions that actually matter here - the ones tutorials tend to skip, because they are less about "how do I write this query" and more about "what happens when three of these queries run at once". If you take one thing away, let it be this: separate reservations from raw quantity, treat inventory as an append-only ledger instead of a mutable number, and model fulfillment per line item instead of per order. Everything below is why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Naive Model Falls Apart
&lt;/h2&gt;

&lt;p&gt;Almost every team starts here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;orders&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;order_items&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;inventory&lt;/code&gt; table with a &lt;code&gt;quantity&lt;/code&gt; column per SKU&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works fine in a demo. It works fine with ten orders a day. It quietly breaks the moment two things happen at once: real concurrency, and real-world messiness like returns, cancellations, and partial shipments.&lt;/p&gt;

&lt;p&gt;The core problem is that this model treats inventory as a single mutable number instead of a series of events and commitments. A &lt;code&gt;quantity&lt;/code&gt; column cannot tell you &lt;em&gt;why&lt;/em&gt; it changed, who is holding a claim on it, or what to undo if an order fails halfway through checkout. You are one race condition away from selling something you do not have.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Want to actually build this instead of just reading about it? &lt;br&gt;
That is the course I am putting together - real schemas, a working system, your own hands on the keyboard. &lt;a href="https://tally.so/r/9qPKJE" rel="noopener noreferrer"&gt;Join the waitlist&lt;/a&gt; - free, in the works now, you will be the first to know.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37g8yyr5g61nhq3ll7xc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37g8yyr5g61nhq3ll7xc.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Entities that actually hold up in production
&lt;/h2&gt;

&lt;p&gt;A model that survives contact with real traffic usually separates a few concepts that the naive version collapses into one.&lt;/p&gt;

&lt;p&gt;Orders and line items are the customer-facing record of what was purchased. Honestly, this part is usually fine even in the naive model - it is not where the trouble starts.&lt;/p&gt;

&lt;p&gt;The trouble starts with inventory reservations. A reservation is a temporary, time-boxed claim on stock, created the moment a customer starts checkout, not when payment succeeds. It is the piece most naive models are missing entirely. In my experience it is the single biggest fix for overselling - more impactful than any amount of locking or retry logic bolted on afterward.&lt;/p&gt;

&lt;p&gt;The other piece is a stock ledger: an append-only log of every change to inventory - received, reserved, released, sold, returned - rather than a single number you overwrite. The current available quantity becomes a derived value, the sum of the ledger, instead of a source of truth you are racing to update every time an order comes in.&lt;/p&gt;

&lt;p&gt;This separation sounds like extra complexity, but it actually removes complexity elsewhere: you stop needing defensive locking scattered across your codebase, because the ledger itself makes concurrent changes safe to reason about.&lt;/p&gt;

&lt;p&gt;A minimal version of the reservation and ledger tables looks something like this - simplified on purpose to show the shape of the idea, not as a copy-paste production schema (a real version would add foreign keys, a unique constraint to prevent double-reserving the same cart, and indexes on &lt;code&gt;sku_id&lt;/code&gt; and &lt;code&gt;expires_at&lt;/code&gt;):&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;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;inventory_reservations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sku_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- 'active', 'committed', 'released'&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;stock_ledger&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sku_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- positive or negative&lt;/span&gt;
  &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- 'received', 'reserved', 'released', 'sold', 'returned'&lt;/span&gt;
  &lt;span class="n"&gt;reference_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- order_id, reservation_id, etc.&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&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;p&gt;Available stock for a SKU is then &lt;code&gt;sum(stock_ledger.delta) - sum(active reservations. quantity)&lt;/code&gt; - never a field you write to directly.&lt;/p&gt;

&lt;p&gt;One honest caveat: summing the entire ledger on every read does not scale. It gets slower every day as the ledger grows. In practice, nobody computes it that way on the hot path. The ledger stays the source of truth for auditing and reconciliation. A fast, denormalized &lt;code&gt;available_quantity&lt;/code&gt; counter, updated transactionally alongside each ledger insert, is what product pages and checkout actually read from. Some teams periodically checkpoint the running total so a full rebuild only sums deltas since the last checkpoint, not the entire history. Reservations stay cheap either way, since you are only ever summing &lt;em&gt;active&lt;/em&gt; reservations - a small, indexed subset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency-Safe Reservations at Checkout
&lt;/h2&gt;

&lt;p&gt;Here is roughly how it plays out - and how it would have prevented the flash-sale scenario above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Customer adds an item to cart and begins checkout.&lt;/li&gt;
&lt;li&gt;The system creates a reservation for that quantity, with a short expiry (commonly 10-15 minutes).&lt;/li&gt;
&lt;li&gt;Available stock is calculated as &lt;code&gt;on_hand - active_reservations&lt;/code&gt;, not read from a single mutable field.&lt;/li&gt;
&lt;li&gt;If payment succeeds, the reservation converts into a committed deduction on the ledger.&lt;/li&gt;
&lt;li&gt;If payment fails or the reservation expires, the stock is released automatically - no manual cleanup, no orphaned holds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The critical design choice is that reservations are &lt;strong&gt;first-class records&lt;/strong&gt;, not just a decremented number. That gives you visibility (you can query "what is currently reserved and by whom"), safety (expiry handles abandoned checkouts), and auditability (you can always explain how a number got to where it is).&lt;/p&gt;

&lt;p&gt;Depending on your database, enforcing this safely under load usually comes down to using row-level locking or conditional writes at the reservation step - the point is that the &lt;em&gt;contention&lt;/em&gt; happens in one well-defined place, instead of being smeared across the codebase.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmmtgjwou5x5ur2ju1sfl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmmtgjwou5x5ur2ju1sfl.png" alt=" " width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part Most Tutorials Skip: Returns and Partial Fulfillment
&lt;/h2&gt;

&lt;p&gt;Almost every ecommerce data-modeling tutorial stops at "customer buys item, stock goes down". Real ecommerce systems have to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A customer returning 1 of 3 items from an order&lt;/li&gt;
&lt;li&gt;A warehouse only having 2 of 3 items in stock, requiring a partial shipment&lt;/li&gt;
&lt;li&gt;A cancellation after payment but before fulfillment&lt;/li&gt;
&lt;li&gt;A return that should restock inventory, but only after inspection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your model represents an order as a single flat status field (&lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;shipped&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;), none of this fits. What actually works is modeling fulfillment &lt;strong&gt;at the line-item level&lt;/strong&gt;, with its own state machine per item - not per order. An order becomes a container for line items that can each be in different states simultaneously. This one change is usually what separates a model that handles real operations from one that only handles the happy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event Sourcing vs. Mutable State: When It is Worth It
&lt;/h2&gt;

&lt;p&gt;You do not need full event sourcing to get most of these benefits - an append-only ledger for inventory changes, layered on top of otherwise normal mutable tables for orders, gets you 80% of the value with a fraction of the complexity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8dpyjqwvqbagkxeq088e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8dpyjqwvqbagkxeq088e.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full event sourcing (where the entire system state is derived from an event log) is worth reaching for when you need strong audit requirements, complex reconciliation with external systems (like warehouse or payment providers), or the ability to replay and debug exactly how state evolved. For most teams, that is overkill for orders, but it is genuinely useful specifically for inventory, where "how did we end up with this number" is a question you will get asked  - usually by finance, usually under pressure.&lt;/p&gt;

&lt;p&gt;One scope note: everything above assumes orders and inventory live close together - one service, one database, one transaction boundary. That is a reasonable starting point, and it is still where a lot of real systems live. But if inventory is split into its own service (increasingly the default in headless/microservices ecommerce setups), reserving stock stops being a single-database transaction and becomes a distributed one - with its own failure modes around what happens when one service confirms a reservation and the other never finds out. That is a big enough topic to deserve its own post rather than a paragraph here. &lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Leaves You
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. It is a handful of deliberate decisions - separating reservations from raw quantity, treating inventory as a ledger instead of a number, modeling fulfillment per line item - that most teams only discover the hard way, usually after their own version of that Friday-night flash sale.&lt;/p&gt;

&lt;p&gt;This post is the overview. I have been writing follow-ups that go deeper on each piece on its own - reservation design and concurrency edge cases, ledger schema patterns, returns and partial fulfillment - since each one has more nuance than fits here.&lt;/p&gt;

&lt;p&gt;This is one of the patterns I keep coming back to when I talk to other engineers - a small set of decisions that quietly determine whether a system holds up in production or falls over under real-world conditions. I am putting together a short, focused course on this exact problem: how to design an order and inventory data model that actually survives concurrency, returns, and scale, with real schemas and walkthroughs rather than abstract theory. If that is useful to you, drop a comment with your own worst inventory-bug story, or &lt;a href="https://tally.so/r/9qPKJE" rel="noopener noreferrer"&gt;join the waitlist&lt;/a&gt; - free, in the works now, you will be the first to know.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>systemdesign</category>
      <category>ecommerce</category>
    </item>
  </channel>
</rss>
