E‑commerce Concurrency Case Study: 12 Interview Questions with Answers
This article provides a self‑test for interview preparation, presenting twelve common interview questions and detailed answers on an e‑commerce concurrency case, covering serial vs async performance, CompletableFuture.allOf, ConcurrentHashMap.compute atomicity, layered cache design, Semaphore rate limiting, ReentrantLock fairness, AtomicInteger CAS, exceptional fallback, double‑layer protection, concurrency safety testing, thread‑pool configuration, and precise cache invalidation.
Q1 Serial vs Async Performance
Serial version calls four interfaces sequentially, total time 530 ms:
ProductInfo product = productClient.getProduct(productId);
PriceInfo price = productClient.getPrice(productId);
StockInfo stock = productClient.getStock(productId);
List<RecommendInfo> recs = productClient.getRecommendations(userId, productId);Async version submits product, price, and stock calls to a thread pool and waits with CompletableFuture.allOf, reducing total time to the slowest call (150 ms). Recommendation is fetched separately because it depends on userId.
CompletableFuture<ProductInfo> productF = CompletableFuture.supplyAsync(() -> productClient.getProduct(productId), executor);
CompletableFuture<PriceInfo> priceF = CompletableFuture.supplyAsync(() -> productClient.getPrice(productId), executor);
CompletableFuture<StockInfo> stockF = CompletableFuture.supplyAsync(() -> productClient.getStock(productId), executor)
.exceptionally(ex -> StockInfo.DEFAULT);
CompletableFuture<Void> allDone = CompletableFuture.allOf(productF, priceF, stockF);
allDone.join();Three independent calls run in parallel. CompletableFuture.allOf waits for all to finish.
Total latency becomes the maximum of the three (150 ms) instead of the sum (530 ms).
Recommendation data is not cached because it is user‑specific and must be queried in real time.
Performance results:
Serial: 530 ms (four calls sequentially).
Concurrent (cache miss): 200 ms (three calls parallel + recommendation 200 ms).
Cache hit: <1 ms for product info (ConcurrentHashMap returns directly).
Improvement: >2.5× faster; thread‑pool reuse avoids thread creation overhead.
Q2 CompletableFuture.allOf
CompletableFuture.allOfcreates a new CompletableFuture that completes only when all supplied futures complete.
CompletableFuture<ProductInfo> productF = ...;
CompletableFuture<PriceInfo> priceF = ...;
CompletableFuture<StockInfo> stockF = ...;
CompletableFuture<Void> allDone = CompletableFuture.allOf(productF, priceF, stockF);
allDone.join(); // blocks until all are doneCompared with Thread.join():
Asynchronous composition vs synchronous blocking.
Supports graceful degradation via exceptionally instead of try‑catch.
Provides result transformation with thenApply and rich composition (anyOf, thenCombine).
Thread‑pool management reuses threads and does not occupy the caller thread.
Q3 ConcurrentHashMap.compute Atomicity
ConcurrentHashMap.computeguarantees that the entire lambda executes atomically for a given key. Only one thread can run the lambda, preventing duplicate remote calls.
return productCache.compute(productId, (k, oldValue) -> {
if (oldValue != null && !isExpired(oldValue)) {
return oldValue; // reuse cached value
}
CompletableFuture<ProductInfo> productF = ...;
CompletableFuture<PriceInfo> priceF = ...;
CompletableFuture<StockInfo> stockF = ...;
CompletableFuture.allOf(productF, priceF, stockF).join();
ProductBaseInfo info = new ProductBaseInfo(productF.join(), priceF.join(), stockF.join());
info.cacheTime = System.currentTimeMillis();
return info; // atomic write back
});Using putIfAbsent only atomically checks existence and inserts; it cannot prevent multiple threads from performing the expensive remote calls, leading to cache penetration. With compute, 20 concurrent threads cause only one actual remote fetch (three calls) while the others read the already computed value.
Q4 Layered Cache Strategy
Cache decisions are based on data characteristics.
Product, price, stock info – cached by productId because the data is identical for all users and has a high hit rate. Cache is invalidated precisely when inventory changes.
Recommendation info – not cached because it is personalized per userId, changes frequently, and the recommendation service already maintains its own cache.
// Get shared product base info from cache
ProductBaseInfo baseInfo = getOrComputeProductBase(productId);
// Real‑time recommendation (no cache)
List<RecommendInfo> recs = productClient.getRecommendations(userId, productId);Q5 Semaphore Rate Limiting
Semaphore is built on AQS shared mode and limits the number of concurrent threads that can enter a critical section.
private final Semaphore inventorySemaphore = new Semaphore(10); // max 10 concurrent deductions
boolean acquired = inventorySemaphore.tryAcquire(acquireTimeoutSeconds, TimeUnit.SECONDS);
if (!acquired) {
throw new IllegalStateException("System busy, please try later");
}
// ... critical section ...
finally {
if (acquired) {
inventorySemaphore.release(); // return permit
}
} tryAcquire(timeout)waits up to the specified time and returns false on timeout; acquire() blocks indefinitely and is unsuitable for user‑facing interfaces. tryAcquire() without timeout returns immediately for fast‑fail scenarios.
Q6 ReentrantLock Fair Lock
private final ReentrantLock stockLock = new ReentrantLock(true); // fair lockFair lock guarantees FIFO acquisition order, preventing thread starvation at the cost of slightly lower throughput. In inventory deduction, fairness is preferred because the operation involves money and user rights.
stockLock.lock();
try {
AtomicInteger stock = stockMap.get(productId);
if (stock == null || stock.get() < quantity) {
return false;
}
stock.getAndAdd(-quantity);
return true;
} finally {
stockLock.unlock();
}Q7 AtomicInteger CAS Decrement
AtomicInteger stock = stockMap.get(productId);
stock.getAndAdd(-quantity); // atomic decrement, returns previous valueCAS (Compare‑And‑Swap) reads the current value, compares it with the expected value, and updates it atomically using the CPU cmpxchg instruction.
Q8 Service Degradation with exceptionally
CompletableFuture<StockInfo> stockF = CompletableFuture.supplyAsync(
() -> productClient.getStock(productId), executor)
.exceptionally(ex -> {
System.out.println("Stock query failed, using default: " + ex.getMessage());
return StockInfo.DEFAULT; // fallback with available = 0
}); StockInfo.DEFAULTis defined as:
public static final StockInfo DEFAULT = new StockInfo(null, 0);When the stock service fails, product and price still display, stock shows “temporarily unavailable”, and page availability improves from 0 % to about 75 %.
Degradation returns fallback data; circuit‑breaker (e.g., Sentinel) is not implemented in this example.
Q9 Double‑Layer Protection
Inventory deduction uses two layers:
// Layer 1: Semaphore – limits overall concurrency
if (!inventorySemaphore.tryAcquire(...)) { throw ...; }
// Layer 2: ReentrantLock – guarantees atomic check‑then‑deduct
stockLock.lock();
try {
AtomicInteger stock = stockMap.get(productId);
if (stock == null || stock.get() < quantity) return false;
stock.getAndAdd(-quantity);
return true;
} finally {
stockLock.unlock();
}Only Semaphore cannot prevent overselling because it does not protect the check‑then‑deduct sequence.
Only ReentrantLock can cause high contention under heavy load.
Both together provide system‑level protection (rate limiting) and business‑level correctness (no oversell).
Q10 Concurrency Safety Test
Test verifies that 20 threads querying the same product cause only one compute execution.
@Test
@DisplayName("Concurrency safety: multiple threads query same product, cache writes once")
void testConcurrentCache_sameProduct() throws InterruptedException {
int threadCount = 20;
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(threadCount);
AtomicInteger invokeCount = new AtomicInteger(0);
ProductClient countingClient = new CountingProductClient(invokeCount);
ProductDetailAsyncService countingService = new ProductDetailAsyncService(countingClient, executor);
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
startLatch.await();
countingService.getOrCompute(1L, 100L);
} finally {
doneLatch.countDown();
}
});
}
startLatch.countDown(); // fire
doneLatch.await(10, TimeUnit.SECONDS);
assertTrue(invokeCount.get() <= 6,
"Concurrent queries of same product, actual invoke count " + invokeCount.get() + " should be <= 6");
}Key points: CountDownLatch synchronizes thread start; CountingProductClient counts remote calls; assertion allows a small extra count due to race, confirming that only one thread performed the expensive computation.
Q11 Thread‑Pool Parameter Configuration
executor = new ThreadPoolExecutor(
10, // corePoolSize
20, // maximumPoolSize
60L, TimeUnit.SECONDS, // keepAliveTime for non‑core threads
new ArrayBlockingQueue<>(100), // bounded work queue
new TestThreadFactory("detail-async-test"),
new ThreadPoolExecutor.CallerRunsPolicy() // back‑pressure
);Behavior:
If core threads are not full, a new core thread is created.
If core threads are full, tasks are queued up to 100.
If the queue is full, non‑core threads are created up to the maximum of 20.
If both threads and queue are exhausted, the caller runs the task, providing throttling.
Using Executors.newFixedThreadPool creates an unbounded LinkedBlockingQueue, which can cause OOM; explicit ThreadPoolExecutor allows a bounded queue and custom rejection policy.
Q12 Precise Cache Invalidation and Expired Cleanup
// Precise invalidation (O(1))
public void invalidateProductCache(Long productId) {
productCache.remove(productId);
}
// Batch cleanup of expired entries
public int cleanExpiredCache() {
AtomicInteger removed = new AtomicInteger(0);
productCache.entrySet().removeIf(entry -> {
if (isExpired(entry.getValue())) {
removed.incrementAndGet();
return true;
}
return false;
});
return removed.get();
}
private boolean isExpired(ProductBaseInfo info) {
return System.currentTimeMillis() - info.cacheTime > CACHE_TTL_MS; // TTL = 5 minutes
}API summary: invalidateProductCache(productId) – O(1) precise removal. cleanExpiredCache() – O(n) batch removal of stale entries. clearCache() – O(1) clear all entries. cacheSize() – O(1) query current size.
This example uses ConcurrentHashMap to illustrate core concurrency primitives; in production, libraries such as Caffeine or Guava Cache provide richer features like LRU eviction, automatic loading, and statistics.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
CodeSmart Hoops
A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
