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)—keyis 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
qtyexceedsperCustomer, return"limit"; - else if
stock < qty, return"sold out"; - else take the stock, create a hold expiring at
nowMs + holdMsand return"held".
- if the customer's active quantity (held + paid) plus
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:
- 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").
- 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.
- Check, then take, in one critical section: in the database this is a conditional
UPDATE … SET stock = stock - qty WHERE stock >= qtyplus 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;
}
}
type Hold = { customer: string; qty: number; expiresAt: number; status: "held" | "paid" | "expired" };
class Reservations {
private results = new Map<string, string>();
private holds = new Map<string, Hold>();
constructor(private stock: number, private perCustomer: number, private holdMs: number) {}
private expire(now: number): void {
for (const h of this.holds.values()) {
if (h.status === "held" && h.expiresAt <= now) {
h.status = "expired";
this.stock += h.qty;
}
}
}
reserve(customer: string, qty: number, key: string, nowMs: number): string {
const previous = this.results.get(key);
if (previous !== undefined) return previous;
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: string;
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: string, nowMs: number): string {
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: number): number {
this.expire(nowMs);
return this.stock;
}
}
class Reservations:
def __init__(self, stock: int, per_customer: int, hold_ms: int):
self.stock = stock
self.per_customer = per_customer
self.hold_ms = hold_ms
self.results: dict[str, str] = {}
self.holds: dict[str, dict] = {}
def _expire(self, now: int) -> None:
for h in self.holds.values():
if h["status"] == "held" and h["expires_at"] <= now:
h["status"] = "expired"
self.stock += h["qty"]
def reserve(self, customer: str, qty: int, key: str, now_ms: int) -> str:
if key in self.results:
return self.results[key]
self._expire(now_ms)
active = sum(h["qty"] for h in self.holds.values()
if h["customer"] == customer and h["status"] != "expired")
if active + qty > self.per_customer:
result = "limit"
elif self.stock < qty:
result = "sold out"
else:
self.stock -= qty
self.holds[key] = {"customer": customer, "qty": qty,
"expires_at": now_ms + self.hold_ms, "status": "held"}
result = "held"
self.results[key] = result
return result
def pay(self, key: str, now_ms: int) -> str:
self._expire(now_ms)
h = self.holds.get(key)
if h is None:
return "unknown"
if h["status"] == "expired":
return "expired"
h["status"] = "paid"
return "paid"
def available(self, now_ms: int) -> int:
self._expire(now_ms)
return self.stock
<?php
class Reservations
{
private array $results = [];
private array $holds = [];
public function __construct(private int $stock, private int $perCustomer, private int $holdMs)
{
}
private function expire(int $now): void
{
foreach ($this->holds as &$h) {
if ($h['status'] === 'held' && $h['expiresAt'] <= $now) {
$h['status'] = 'expired';
$this->stock += $h['qty'];
}
}
unset($h);
}
public function reserve(string $customer, int $qty, string $key, int $nowMs): string
{
if (array_key_exists($key, $this->results)) {
return $this->results[$key];
}
$this->expire($nowMs);
$active = 0;
foreach ($this->holds as $h) {
if ($h['customer'] === $customer && $h['status'] !== 'expired') {
$active += $h['qty'];
}
}
if ($active + $qty > $this->perCustomer) {
$result = 'limit';
} elseif ($this->stock < $qty) {
$result = 'sold out';
} else {
$this->stock -= $qty;
$this->holds[$key] = ['customer' => $customer, 'qty' => $qty, 'expiresAt' => $nowMs + $this->holdMs, 'status' => 'held'];
$result = 'held';
}
return $this->results[$key] = $result;
}
public function pay(string $key, int $nowMs): string
{
$this->expire($nowMs);
if (!isset($this->holds[$key])) {
return 'unknown';
}
if ($this->holds[$key]['status'] === 'expired') {
return 'expired';
}
$this->holds[$key]['status'] = 'paid';
return 'paid';
}
public function available(int $nowMs): int
{
$this->expire($nowMs);
return $this->stock;
}
}
class Reservations {
private static final class Hold {
final String customer; final int qty; final long expiresAt; String status = "held";
Hold(String customer, int qty, long expiresAt) { this.customer = customer; this.qty = qty; this.expiresAt = expiresAt; }
}
private int stock;
private final int perCustomer;
private final int holdMs;
private final Map<String, String> results = new HashMap<>();
private final Map<String, Hold> holds = new HashMap<>();
public Reservations(int stock, int perCustomer, int holdMs) {
this.stock = stock;
this.perCustomer = perCustomer;
this.holdMs = holdMs;
}
private void expire(int now) {
for (Hold h : holds.values()) {
if (h.status.equals("held") && h.expiresAt <= now) {
h.status = "expired";
stock += h.qty;
}
}
}
public String reserve(String customer, int qty, String key, int nowMs) {
if (results.containsKey(key)) return results.get(key);
expire(nowMs);
int active = 0;
for (Hold h : holds.values()) if (h.customer.equals(customer) && !h.status.equals("expired")) active += h.qty;
String result;
if (active + qty > perCustomer) result = "limit";
else if (stock < qty) result = "sold out";
else {
stock -= qty;
holds.put(key, new Hold(customer, qty, (long) nowMs + holdMs));
result = "held";
}
results.put(key, result);
return result;
}
public String pay(String key, int nowMs) {
expire(nowMs);
Hold h = holds.get(key);
if (h == null) return "unknown";
if (h.status.equals("expired")) return "expired";
h.status = "paid";
return "paid";
}
public int available(int nowMs) {
expire(nowMs);
return 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.)
More Design & Data Structures problems
- LRU Cache · Medium
- Token Bucket Rate Limiter · Medium
- Key-Value Store with TTL · Medium
- URL Shortener Codes (Base62) · Easy
- Build an EventEmitter · Medium
- Row Lock Manager with Deadlock Detection · Hard