A Semaphore-Based Flash Sale System Survives ByteDance's Third-Round Coding Gauntlet
Flash sale problems are a staple of Chinese big-tech backend interviews, and the `AtomicInteger` check-then-act trap is a reliable filter for candidates who understand atomicity versus compound operations. The `Semaphore` approach generalizes to any resource-pool scenario where a fixed number of permits must be acquired with a deadline and released safely.
The core challenge is a classic overselling problem: 100 threads race to claim 30 items, each thread must acquire a lock within 3 seconds or bail, and any successful claim must be rolled back if payment takes longer than 10 seconds. The solution replaces the obvious-but-broken `AtomicInteger` check-then-decrement pattern with a `Semaphore` whose `tryAcquire` method bundles the check and decrement into a single atomic CAS operation, eliminating the race window entirely.
A wrapper class `SemaphoreAndStocks` pairs each semaphore with its initial stock count so that `undoDecrement` can guard against over-releasing permits — calling `release()` unconditionally would inflate available permits beyond the original 30. The payment timeout logic lives outside `decrementStock` in the main thread pool, where each `Callable` sleeps for a random duration up to 11 seconds and returns an "undo" signal if it exceeds 10 seconds, triggering a rollback.
The main loop runs rounds of `invokeAll` until stock hits zero, collecting per-thread success counts and undo events. The design keeps concerns separated: `decrementStock` only gates access, while the payment simulation and timeout enforcement happen in the caller.
Interview coding rounds at ByteDance's payments division still demand raw API recall — no IDE autocomplete, no AI copilot — which punishes developers who have outsourced syntax memory to tools.
The `Semaphore` solution is cleaner than a `ReentrantLock` with `tryLock` because the permit count directly models inventory; a lock would require a separate counter and still need atomic check-and-decrement logic.
Running all 100 callables every round via `invokeAll` is deliberately wasteful and only works because `tryAcquire` fails fast when permits are exhausted; a production system would use a bounded queue or a countdown latch to avoid re-submitting losers.
The guard `semaphore.availablePermits() < initialStock` before `release()` is a defensive pattern that prevents silent state corruption from duplicate rollback calls.
Separating the lock-acquisition timeout (3s) from the payment timeout (10s) mirrors real payment gateway architecture, where the acquire phase is a fast mutex and the payment phase is an async external call with its own SLA.