A Semaphore-Based Flash Sale System Survives ByteDance's Third-Round Coding Gauntlet
ByteDance International Payments - Backend Development - Third Round Interview Experience
This round was purely a hands-on coding session, no Q&A.
Although this wasn't a difficult problem, I couldn't code it out at the time. The main reason is that I've become too reliant on AI for writing code in my daily work and projects. I haven't seriously memorized many Java APIs. Apart from the APIs I frequently use for LeetCode problems, I can only read and understand others but have no idea how to use them myself. So I plan to use technical blog posts to fill this gap.
Problem Description
Design a flash sale system where 100 threads compete to buy a product with a stock of 30, ensuring thread safety. If a thread exceeds 3 seconds when calling the flash sale interface (acquiring the lock), the attempt is considered timed out and the thread gives up. If a thread successfully gets a flash sale opportunity but does not complete the payment within 10 seconds, the flash sale is invalidated, and the inventory must be rolled back.
The following skeleton is provided and can be modified freely. Input and output can be set up arbitrarily, meaning Scanner input is not mandatory.
public class Redeem {
// Initialize product ID and corresponding quantity
public void init(int goodId, int stock) {}
// Flash sale entry point; returns order ID if successful (I directly return the thread name here)
public String redeemGood(int goodId) {}
// Decrease stock
public boolean decrementStock(int goodId) {}
// Rollback stock
public void undoDecrement(int goodId) {}
// Check current stock quantity
public int checkStock(int goodId) {}
// Main function
}
My thinking was very chaotic at the time. First, I immediately added synchronized to redeemGood, then used ConcurrentHashMap<Integer, AtomicInteger> to store (goodId, stock). I didn't know how to judge timeout within a synchronized block, and I didn't think of ReentrantLock at the time, so I simply skipped implementing those interfaces and started writing the multi-threaded invocation logic in the main function.
I planned to use a thread pool to invokeAll on 100 Callable tasks. Even though I had seriously studied the multi-threaded coding problem I failed in the second round the day before the interview, I forgot how to declare newFixedThreadPool and how to write the rejection policy for a regular thread pool. In the end, it was a complete mess.
I've seen some interview experience posts on Xiaohongshu saying that if you can't code it out, some interviewers will explain the solution approach to you. But the interviewer I met didn't explain the approach to me. I don't know if it was because they were too busy, or because their team's tech stack is Go, or some other reason.
In the end, the interviewer asked me to explain my approach myself, and they didn't interrupt me at all. Then we moved to the reverse Q&A session. It was only after following up with HR for over a week that I found out I failed the interview.
Approach
After the interview, I realized I could use tryLock from ReentrantLock to control the lock acquisition timeout limit (the classic case of only thinking of the solution to the hardest problem after the exam is over).
Finally, I used CodeBuddy + DeepSeek to review, letting AI guide my learning. At each step, I had AI evaluate my implementation and give improvement suggestions.
Design of init
Actually, the design of this interface is quite simple. Although we only need to run one test case here, to demonstrate consideration for thread safety, we can use ConcurrentHashMap to store (goodId, stock). The next question is: what data structures correspond to goodId and stock respectively?
goodId can be considered immutable, with no race condition issues, so Integer can be used directly.
stock is modified by multiple threads, so it must be a thread-safe data structure. The hint AI gave me was to use Semaphore.
The reason for not using AtomicInteger can be illustrated by the following two examples:
Example 1: Assume the current stock is 1, and threads A and B execute simultaneously:
// ❌ Wrong approach: check then decrement
if (stock.get() > 0) { // Time t1: A reads 1
// Time t2: B reads 1 (A hasn't decremented yet!)
stock.decrementAndGet(); // t3: A changes 1 → 0 ✅
// t4: B changes 0 → -1 ❌ Oversold!
return true;
}
The problem is: get() + decrementAndGet() are two independent operations. Although each is atomic individually, together they are not. Thread B passed the > 0 check before A performed the decrement.
Example 2: The cumbersome correct approach
// ✅ Correct approach: CAS spin
int current;
do {
current = stock.get(); // Read current value
if (current <= 0) return false; // Sold out, quick return
} while (!stock.compareAndSet(current, current - 1)); // CAS: decrement only if expected value hasn't changed
return true;
If using Semaphore, then semaphore.tryAcquire itself is a CAS operation, eliminating the need to manually write spin attempts.
So the final choice should be ConcurrentHashMap<Integer, Semaphore>?
Not exactly. It should be ConcurrentHashMap<Integer, SemaphoreAndStocks>
public SemaphoreAndStocks(Semaphore semaphore, int stock) {
this.semaphore = semaphore;
this.initialStock = stock;
}
The initial value of stock is needed later to avoid problems caused by semaphore.release().
semaphore.release()calls are unrestricted. If called without judgment, it could causesemaphore.availablePermits()to exceed the initially set value of 30.
Design of redeemGood
The business logic is in decrementStock. This only serves as the entry point for the flash sale interface. If the flash sale qualification is successfully obtained, return the current thread name; otherwise, return "null".
public String redeemGood(int goodId) {
if (decrementStock(goodId)) {
return Thread.currentThread().getName();
}
return "null";
}
Design of decrementStock
It's also quite simple. Consider avoiding NPE situations, then use tryAcquire. Each successful tryAcquire reduces semaphore.availablePermits() by one.
When semaphore.availablePermits() is 0, tryAcquire can fail quickly.
The stock rollback strategy is not inserted here because this only secures the flash sale qualification and hasn't reached the payment stage. The actual payment stage is simulated in the main function.
The reason for not simulating the payment stage here is that calculating payment duration should not be operated within stock decrement. AI explained to me that the payment stage would be handled in another thread.
public boolean decrementStock(int goodId) {
SemaphoreAndStocks sas = map.get(goodId);
if (sas == null) {
return false;
}
Semaphore semaphore = sas.getSemaphore();
try {
if (!semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)) {
return false;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return true;
}
Design of undoDecrement
Nothing particularly difficult.
public void undoDecrement(int goodId) {
SemaphoreAndStocks sas = map.get(goodId);
if (sas == null) {
return;
}
Semaphore semaphore = sas.getSemaphore();
if (semaphore.availablePermits() < sas.getInitialStock()) {
semaphore.release();
}
}
Final Code
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* ByteDance International Payments - Backend Development - Third Round<br>
* Problem Description: Design a simple flash sale system. Given a product ID and its quantity, assume the quantity is 30, and 100 threads call the flash sale interface.
*/
public class Redeem {
public static Random rand = new Random(2026);
private static class SemaphoreAndStocks {
private final Semaphore semaphore;
private final int initialStock;
public SemaphoreAndStocks(Semaphore semaphore, int stock) {
this.semaphore = semaphore;
this.initialStock = stock;
}
public Semaphore getSemaphore() {
return semaphore;
}
public int getInitialStock() {
return initialStock;
}
}
private final ConcurrentHashMap<Integer, SemaphoreAndStocks> map = new ConcurrentHashMap<>();
private static final long TIMEOUT = 3000;
private static final int MAX_THREAD = 100;
/**
* Initialize product ID and its corresponding initial quantity
* @param goodId Product ID
* @param stock Initial product quantity
*/
public void init(int goodId, int stock) {
this.map.putIfAbsent(goodId, new SemaphoreAndStocks(new Semaphore(stock), stock));
}
/**
* Flash sale interface, must return result within 3s; if interface response time exceeds 3s, the current thread gives up continuing the flash sale<br>
* If product quantity is less than or equal to 0, it means the current product is sold out, and this interface should respond quickly.
* @param goodId Product ID
* @return Returns thread name on success, returns string "null" on failure
*/
public String redeemGood(int goodId) {
if (decrementStock(goodId)) {
return Thread.currentThread().getName();
}
return "null";
}
/**
* Decrement product quantity
* @param goodId Product ID
* @return Returns {@code true} if decrement succeeds, otherwise returns {@code false}
*/
public boolean decrementStock(int goodId) {
SemaphoreAndStocks sas = map.get(goodId);
if (sas == null) {
return false;
}
Semaphore semaphore = sas.getSemaphore();
try {
if (!semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)) {
return false;
}
// Payment simulation?
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return true;
}
/**
* Rollback product quantity. Rollback is needed in this scenario: user clicked the flash sale button, but did not pay within 10s.
* @param goodId Product ID
* @return
*/
public void undoDecrement(int goodId) {
SemaphoreAndStocks sas = map.get(goodId);
if (sas == null) {
return;
}
Semaphore semaphore = sas.getSemaphore();
if (semaphore.availablePermits() < sas.getInitialStock()) {
semaphore.release();
}
}
/**
* Return remaining product quantity
* @param goodId Product ID
* @return Remaining product quantity
*/
public int checkStock(int goodId) {
SemaphoreAndStocks sas = map.get(goodId);
if (sas == null) {
return 0;
}
return sas.getSemaphore().availablePermits();
}
/**
* Set input arbitrarily
* @param args
*/
public static void main(String[] args) {
int goodId = 1248;
long payTimeout = 10_000;
Redeem redeem = new Redeem();
redeem.init(goodId, 30);
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(100);
List<Callable<String>> callableList = new ArrayList<>(MAX_THREAD);
AtomicInteger undo = new AtomicInteger();
for (int i = 0; i < MAX_THREAD; i++) {
callableList.add(() -> {
String res = redeem.redeemGood(goodId);
if ("null".equals(res)) {
return res;
}
// Simulate payment stage
long t0 = System.currentTimeMillis();
Thread.sleep(rand.nextLong(0, 11_000));
long t1 = System.currentTimeMillis();
return t1 - t0 < payTimeout ? res : "undo";
});
}
Map<String, Integer> threadAndRedeemTimes = new LinkedHashMap<>();
int counter = 0;
while (redeem.checkStock(goodId) > 0) {
try {
// Round-by-round flash sale
var futureList = fixedThreadPool.invokeAll(callableList);
// Iterate through the results of each thread in the current round
for (Future<String> future: futureList) {
try {
String s = future.get();
// Payment timeout, rollback needed
if ("undo".equals(s)) {
redeem.undoDecrement(goodId);
undo.incrementAndGet();
} else if (!"null".equals(s)) {
++counter;
threadAndRedeemTimes.merge(s, 1, Integer::sum);
}
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
for (var entry: threadAndRedeemTimes.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
System.out.println();
System.out.println("========== Final Results ==========");
System.out.println("Remaining stock: " + redeem.checkStock(goodId));
System.out.println("Undo count: " + undo.get());
System.out.println("Success count: " + counter);
fixedThreadPool.shutdown();
}
}
Reflections
In this current era, although everyone jokes that hand-coding is ancient programming and the future belongs to natural language programming, I personally feel that hand-coding is still indispensable.
Understanding and controlling every detail of a system architecture requires personally designing it once and stepping into all kinds of pitfalls to know what the real situation is like.
I also enjoy using AI to write code, and I also enjoy hand-building a complex system. Both bring me joy, but one is short-term joy, and the other is a joy that can be savored repeatedly.