Coding problem · Medium · Design & Data Structures

Flash-Sale Reservations Without Overselling

Medium Practical tasks

A sneaker drop sells limited stock. Implement the reservation logic that runs inside the database transaction.

Reservations(stock, perCustomer, holdMs):

  • reserve(customer, qty, key, nowMs) — key is the client's idempotency key. If the key was used before, return the same result as the first time, without side effects. Otherwise, first expire holds whose expiry time is ≤ nowMs (their stock returns). Then:
    • if the customer's active quantity (held + paid) plus qty exceeds perCustomer, return "limit";
    • else if stock < qty, return "sold out";
    • else take the stock, create a hold expiring at nowMs + holdMs and return "held".
  • pay(key, nowMs) — expire due holds first. Return "paid" if the key's hold is active (mark it paid) or already paid, "expired" if it expired, and "unknown" for keys without a hold.
  • available(nowMs) — expire due holds, then return the remaining stock.

See the capstone Never Oversell a Flash Sale.

Example 1

Input: ["Reservations","reserve","reserve","available","reserve","reserve","available","pay","available","pay"]
[[2,2,1000],["ada",1,"k1",0],["ada",1,"k1",10],[10],["bob",2,"k2",20],["bob",1,"k3",30],[30],["k1",500],[1500],["k3",1600]]
Output: [null,"held","held",1,"sold out","held",0,"paid",1,"expired"]
Explanation: Ada's retry with the same key doesn't take a second pair. Bob's hold expires at 1030, so his stock returns.

Hints

Hint 1

Store each key's first result for idempotent replays.

Hint 2

Expire lazily at the start of every call (compare expiry ≤ now).

Hint 3

Active quantity per customer = holds not expired + paid.

Write and run your solution

Loading the editor… (JavaScript is required to run code in your browser.)

Solution and explanation

Show the solution

Time O(holds) per call with lazy expiry (a heap makes expiry O(log n)) · Space O(keys)

Three ideas from real checkout systems in one class:

  1. Idempotency: remember the first response per key and replay it, so a client retry after a timeout can't take stock twice (and a retried "sold out" stays "sold out").
  2. Holds with expiry: stock is reserved, not sold, until payment; expired holds give stock back. Expiring lazily at the start of each call keeps it simple and deterministic.
  3. Check, then take, in one critical section: in the database this is a conditional UPDATE … SET stock = stock - qty WHERE stock >= qty plus a per-customer check under a lock on the customer row.

Reference solution

class Reservations {
  constructor(stock, perCustomer, holdMs) {
    this.stock = stock;
    this.perCustomer = perCustomer;
    this.holdMs = holdMs;
    this.results = new Map(); // key -> first result
    this.holds = new Map();   // key -> { customer, qty, expiresAt, status }
  }

  expire(now) {
    for (const h of this.holds.values()) {
      if (h.status === "held" && h.expiresAt <= now) {
        h.status = "expired";
        this.stock += h.qty;
      }
    }
  }

  reserve(customer, qty, key, nowMs) {
    if (this.results.has(key)) return this.results.get(key);
    this.expire(nowMs);
    let active = 0;
    for (const h of this.holds.values()) {
      if (h.customer === customer && h.status !== "expired") active += h.qty;
    }
    let result;
    if (active + qty > this.perCustomer) result = "limit";
    else if (this.stock < qty) result = "sold out";
    else {
      this.stock -= qty;
      this.holds.set(key, { customer, qty, expiresAt: nowMs + this.holdMs, status: "held" });
      result = "held";
    }
    this.results.set(key, result);
    return result;
  }

  pay(key, nowMs) {
    this.expire(nowMs);
    const h = this.holds.get(key);
    if (!h) return "unknown";
    if (h.status === "expired") return "expired";
    h.status = "paid";
    return "paid";
  }

  available(nowMs) {
    this.expire(nowMs);
    return this.stock;
  }
}

Common mistakes

  • Taking stock again on a retried key.
  • Counting expired holds against the customer limit.
  • Using < instead of ≤ for expiry.
  • Letting pay() revive an expired hold.

Follow-up questions

  • Store holds in a min-heap by expiry.
  • What changes when several app servers share the stock? (Database row + conditional update.)