</>longpham.tech
All posts
·7 min read

Everyone Adds to Cart, Few Check Out

A flash sale is the same contended inventory as ticketing, but the winning design is the opposite: oversubscribe the cart and decrement only at checkout. A runnable lab measuring the funnel, a payment-TTL stock reclaim, and a per-user anti-scalper cap.

#concurrency#e-commerce#architecture#distributed-systems

A flash sale is the same contended, finite inventory as selling tickets — but the correct design is the opposite of a seat map. A ticket seat is reserved the moment it's in your cart; a flash-sale item is not, and that's on purpose. You've felt it: at 10:00 sharp everyone can add the item to their cart, but most people hit "sold out" at checkout. That's not a bug — it's the right architecture, and here's why, measured.

Runnable companion: ecommerce-flash-sale on GitHub. make funnel && make timeout && make limit reproduces every number below (pure Python, no deps). Sibling: high-demand-ticketing.

Decrement at checkout, not in the cart

The key decision is when stock leaves inventory. Reserve-at-cart (ticketing style) decrements on add-to-cart; deduct-at-checkout (flash-sale style) touches nothing until the order is placed. With 400 shoppers, 100 units, as cart abandonment rises:

abandon   strategy             add-to-cart   sold   wasted
  20%     reserve-at-cart              25%     80       20
          deduct-at-checkout          100%    100        0
  40%     reserve-at-cart              25%     60       40
          deduct-at-checkout          100%    100        0
  60%     reserve-at-cart              25%     40       60
          deduct-at-checkout          100%    100        0

reserve-at-cart locks a unit for every abandoner, so the cart "sells out" to holds — only 25% of shoppers even get an item into the cart, and at 40% abandonment it sells just 60 of 100 units. deduct-at-checkout keeps 100% of stock available to buyers who actually convert, so it sells the full 100 at every abandonment rate. "Everyone adds to cart, few check out" is the design working: it refuses to waste stock on carts that will be abandoned.

Pending orders need a payment TTL

Deducting at checkout leaves a smaller leak: a buyer places the order (stock −1, pending) then never pays. A TTL cancels the unpaid order and returns the unit to waiting demand:

non-payment    no timeout: sold    with timeout: sold
    10%             90 (90%)             100 (100%)
    30%             70 (70%)             100 (100%)
    50%             50 (50%)             100 (100%)

At 30% non-payment you sell only 70% of stock without a TTL; the timeout brings it back to ~100%. It's the seat-hold + reaper pattern from ticketing, moved to the order level.

A per-user cap stops scalpers

Flash sales attract bots — a few accounts firing many requests each. 100 units, 200 genuine buyers (one request) + 20 scalpers (50 requests each):

policy                  to scalpers   unique buyers   most by one
no limit                    83 (83%)             36           8.1
per-user limit = 1          20 (20%)            100           1.0

With no cap, stock goes to whoever sends the most requests — scalpers scoop 83% and only ~36 people are served. A one-per-account cap drops that to 20% and spreads the drop across all 100 unique buyers. It's the inventory-side half of anti-bot defense (pair with auth, rate limits, bot filtering).

What carries over from ticketing

The checkout itself still needs the mechanics proven in the ticketing lab, unchanged: an atomic decrement so a burst of orders never oversells (buying N units is one all-or-nothing update), idempotent payment on a client key, and a virtual waiting room for backpressure when the spike is large. The one real difference is risk appetite: ticketing must never oversell; e-commerce often tolerates a small deliberate oversell (backorder / refund) to lift conversion — a product decision, not a correctness one.

What I'd say in an interview

  • Reserve stock in the cart only for unique, rarely-abandoned units (a seat). For fungible goods with high abandonment, oversubscribe the cart and decrement at checkout — you sell the full stock instead of wasting it on abandoners (measured: 60 → 100 of 100 units at 40% abandonment).
  • Put a TTL on the pending order so non-payers don't strand inventory — the reaper pattern at the order level (70% → ~100% utilization).
  • Cap purchases per user to blunt scalpers (83% → 20% of stock), paired with auth and bot filtering.
  • The checkout decrement is still atomic and payment idempotent — same invariants as ticketing; only the reservation point and the oversell tolerance change.