<?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: Daniel Cruz</title>
    <description>The latest articles on DEV Community by Daniel Cruz (@daniel_cruz_aaf7dd957e107).</description>
    <link>https://dev.to/daniel_cruz_aaf7dd957e107</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%2F4234646%2Fa548c369-7784-4c62-b8f4-eacf6396fa40.png</url>
      <title>DEV Community: Daniel Cruz</title>
      <link>https://dev.to/daniel_cruz_aaf7dd957e107</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daniel_cruz_aaf7dd957e107"/>
    <language>en</language>
    <item>
      <title>Stopping Black Friday Oversells: A Stock Reservation Data Model</title>
      <dc:creator>Daniel Cruz</dc:creator>
      <pubDate>Tue, 10 Nov 2026 03:45:39 +0000</pubDate>
      <link>https://dev.to/daniel_cruz_aaf7dd957e107/stopping-black-friday-oversells-a-stock-reservation-data-model-5do7</link>
      <guid>https://dev.to/daniel_cruz_aaf7dd957e107/stopping-black-friday-oversells-a-stock-reservation-data-model-5do7</guid>
      <description>&lt;h2&gt;
  
  
  The bug that only shows up during the sale
&lt;/h2&gt;

&lt;p&gt;Most checkout flows work fine on a normal Tuesday. You read the stock count, check that it is above zero, take payment, and decrement it. The flaw stays hidden until many buyers go after the same discounted item in the same few seconds. Then two requests both read &lt;code&gt;stock = 1&lt;/code&gt;, both pass the check, and both charge a card for an item that only exists once.&lt;/p&gt;

&lt;p&gt;This post walks through a data model that closes that gap. It uses short-lived reservations, atomic updates, and idempotent payment handling. The examples use PostgreSQL syntax, but the ideas carry over to any store that supports transactions or atomic conditional writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why check-then-write fails
&lt;/h2&gt;

&lt;p&gt;The naive version looks like this:&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="c1"&gt;-- Request A and Request B run this at the same time&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- both see 1&lt;/span&gt;
&lt;span class="c1"&gt;-- application code: if stock &amp;gt; 0 then...&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stock&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- both succeed, stock = -1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The read and the write are separate steps, and nothing ties them together. Wrapping them in a transaction does not fix this by itself under the default isolation level. The fix is to make the condition part of the write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Make the decrement conditional and atomic
&lt;/h2&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;products&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;available&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="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;
&lt;span class="k"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no row comes back, the item is sold out and the request fails cleanly. The database serializes the row update, so two buyers cannot both take the last unit. Add a &lt;code&gt;CHECK (available &amp;gt;= 0)&lt;/code&gt; constraint as a backstop, so a bug elsewhere fails loudly instead of quietly producing negative stock.&lt;/p&gt;

&lt;p&gt;That solves the race. It does not solve a second problem: stock taken by people who never finish paying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Separate "available" from "sold" with reservations
&lt;/h2&gt;

&lt;p&gt;If you decrement at add-to-cart, abandoned carts lock up inventory for the whole sale. If you decrement only at payment capture, you are back to overselling. A reservation table sits between those two options:&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;stock_reservations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="kt"&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;product_id&lt;/span&gt;   &lt;span class="kt"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="nf"&gt;products&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="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cart_id&lt;/span&gt;      &lt;span class="kt"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;qty&lt;/span&gt;          &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qty&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="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;       &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'held'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'committed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'released'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt;   &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&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="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&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;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;stock_reservations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_at&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;The lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hold&lt;/strong&gt; when the buyer starts checkout (not at add-to-cart). Run the conditional decrement on &lt;code&gt;products.available&lt;/code&gt; and insert a &lt;code&gt;held&lt;/code&gt; row with an expiry, in one transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit&lt;/strong&gt; when payment succeeds. Flip the status to &lt;code&gt;committed&lt;/code&gt;. Stock is already gone from &lt;code&gt;available&lt;/code&gt;, so nothing else changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release&lt;/strong&gt; when the hold expires or payment fails. Flip it to &lt;code&gt;released&lt;/code&gt; and add the quantity back to &lt;code&gt;available&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pick the hold duration based on how long your real checkout takes, including 3-D Secure or bank redirects. Show the buyer a timer so the expiry does not surprise them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Release expired holds safely
&lt;/h2&gt;

&lt;p&gt;A background worker reclaims expired reservations. Several workers can run it at once without fighting if each one skips rows that another has locked:&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;WITH&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&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;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stock_reservations&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'held'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&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;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="no"&gt;SKIP&lt;/span&gt; &lt;span class="no"&gt;LOCKED&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;released&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;stock_reservations&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'released'&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sum&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;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;released&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The locking clause is documented in the &lt;a href="https://www.postgresql.org/docs/current/sql-select.html" rel="noopener noreferrer"&gt;PostgreSQL SELECT reference&lt;/a&gt;. Because the status filter is &lt;code&gt;held&lt;/code&gt;, a reservation that was committed a moment earlier is never released by mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Make payment confirmation idempotent
&lt;/h2&gt;

&lt;p&gt;The commit step is triggered by a payment event, and payment events are messy. Buyers double-click, mobile networks retry, and webhooks can arrive more than once or out of order. Two rules keep this under control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Send an idempotency key on the payment request.&lt;/strong&gt; Use the reservation or order ID, so a retried request does not create a second charge. Stripe, for example, documents this pattern in its &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;idempotent requests guide&lt;/a&gt;. Check your own provider's equivalent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the commit a conditional state transition.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&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;stock_reservations&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'committed'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;reservation_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'held'&lt;/span&gt;
&lt;span class="k"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&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 row comes back, this is the first time you have processed the event. If it does not, either the event was already handled (do nothing) or the hold expired before payment landed. That last case is the one teams forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edge case: paid after the hold expired
&lt;/h2&gt;

&lt;p&gt;A buyer who stalls on a bank redirect can pay after the worker has released their units. Decide your policy before the sale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;on payment_succeeded(reservation):
  if commit(reservation) succeeds: fulfil
  else if reservation.status == 'released':
      try conditional decrement on products.available
      if it succeeds: create committed reservation, fulfil
      else: refund automatically, notify buyer, log for support
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automatic refunds with a clear message are better than a support team finding the oversell by hand days later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping hot rows from becoming a bottleneck
&lt;/h2&gt;

&lt;p&gt;When one doorbuster SKU gets most of the traffic, every checkout queues on the same row. Options, from simplest to most complex:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the transaction that touches &lt;code&gt;products&lt;/code&gt; as short as possible. No network calls inside it.&lt;/li&gt;
&lt;li&gt;Split stock for a hot SKU into several bucket rows and decrement a random bucket with stock left.&lt;/li&gt;
&lt;li&gt;Move the counter for that SKU into an atomic in-memory counter (for example, a Redis &lt;code&gt;DECRBY&lt;/code&gt; guarded by a script) and reconcile it to the database. This is fast, but now you have two sources of truth to reconcile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with the first option and measure before you reach for the third.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-sale verification list
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A concurrency test that fires many parallel checkouts at a SKU with stock of 1 and asserts exactly one success.&lt;/li&gt;
&lt;li&gt;A replay test that delivers the same payment webhook several times.&lt;/li&gt;
&lt;li&gt;A test for payment arriving after expiry.&lt;/li&gt;
&lt;li&gt;A dashboard for &lt;code&gt;available&lt;/code&gt;, &lt;code&gt;held&lt;/code&gt; and &lt;code&gt;committed&lt;/code&gt; per hot SKU.&lt;/li&gt;
&lt;li&gt;An alert if the expiry worker falls behind.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inventory is only one part of getting a store ready for a sale. Traffic, speed, payments and support all need attention too. The team at &lt;a href="https://disruptivestudio.com/blog/prepare-website-black-friday-checklist" rel="noopener noreferrer"&gt;Disruptive Studio&lt;/a&gt; covers the broader checklist. If your stock counts hold up under concurrent checkouts, you avoid the most expensive kind of Black Friday bug: one that costs you money and annoys the customer.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>database</category>
      <category>ecommerce</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
