When given this prompt in an interview, never start coding immediately. Begin by establishing the scope. Here are the exact clarifying questions you should ask:
- Topology: Are we designing for a single warehouse or a distributed, multi-warehouse setup? (We chose: Multi-warehouse).
- Granularity: Do we track aggregate counts (e.g., 500 iPhones) or individual physical items? (We chose: Individual items with unique barcodes and exact bin locations).
- Concurrency: Do we need to handle flash sales safely where thousands of users try to buy the same item simultaneously? (We chose: Yes, strict thread safety is required).
- Features: Are we handling inbound restocking and alerts, or just outbound fulfillment? (We chose: Both inbound and outbound, plus low-stock alerts).
To model the physical world accurately, we need clear boundaries between our data objects.
Product: The catalog metadata (ID, Name, Price). It does not care about physical location.Item: A specific physical box in the real world. It has a Barcode, belongs to a Product, and has a physical Bin Location.Warehouse: The local storage facility. It manages its own internal thread-safe inventory and locking mechanisms.InventoryManager: The global orchestrator. It knows about all warehouses, handles routing, and broadcasts system-wide alerts.
Interviewers look for SDE-2s to justify their architectural choices using standard patterns.
-
State Pattern (
ItemStateEnum): We strictly control an item's lifecycle (AVAILABLE$\rightarrow$ RESERVED$\rightarrow$ SHIPPED). -
Singleton Pattern (
InventoryManager): Guarantees a single, centralized orchestrator in memory to manage the distributed warehouses. -
Strategy Pattern (
WarehouseSelectionStrategy): Decouples routing logic from the core manager (Open/Closed Principle). -
Observer Pattern (
InventoryObserver): Handles low-stock alerts, keeping the inventory domain decoupled from notification domains.
This is the most critical part of the interview. When multiple threads try to buy the last unit of a product, you must prevent overselling.
-
ConcurrentHashMap<String, Queue<Item>>: Gives us thread-safe,$O(1)$ lookups for inventory. -
ConcurrentLinkedQueue<Item>: A highly optimized, lock-free queue for holding the physical items. -
Fine-Grained Locking (
ReentrantLock): We maintain a map of locks per Product ID. By locking at the Product ID level instead of the whole method, only threads competing for the exact same item are forced to wait, maximizing throughput.
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
// ==========================================
// 1. CORE ENTITIES & STATE
// ==========================================
/* INTERVIEW EXPLANATION: "I am using an Enum for ItemState to strictly control the lifecycle of a physical unit.
If a flash sale occurs, we don't just delete an item from the database when someone clicks 'Checkout'.
We transition it to RESERVED. If their payment fails, a background job can easily find RESERVED items past
their TTL and flip them back to AVAILABLE. This prevents phantom reads and lost inventory." */
enum ItemState {
AVAILABLE, RESERVED, SHIPPED, DAMAGED
}
class Product {
private String productId;
private String name;
private double price;
public Product(String productId, String name, double price) {
this.productId = productId;
this.name = name;
this.price = price;
}
public String getProductId() { return productId; }
public String getName() { return name; }
}
class Item {
private String barcode;
private String productId;
private ItemState state;
private String binLocation;
public Item(String barcode, String productId, String binLocation) {
this.barcode = barcode;
this.productId = productId;
this.binLocation = binLocation;
this.state = ItemState.AVAILABLE;
}
public String getProductId() { return productId; }
public String getBarcode() { return barcode; }
public ItemState getState() { return state; }
public void setState(ItemState state) { this.state = state; }
}
class Location {
double lat, lon;
public Location(double lat, double lon) { this.lat = lat; this.lon = lon; }
}
// ==========================================
// 2. WAREHOUSE (Concurrency Engine)
// ==========================================
/* INTERVIEW EXPLANATION: "To handle high concurrency during flash sales, I am designing the Warehouse class
to be highly thread-safe. I chose a ConcurrentHashMap to store the available inventory, giving us O(1) lock-free lookups.
However, simply using concurrent collections isn't enough to prevent 'overselling' if 10,000 users try to buy
the last 5 items.
To solve this, I introduced 'productLocks'—a map of ReentrantLocks per Product ID.
Why per product? If I locked the whole warehouse, an order for an iPhone would block an order for a MacBook,
destroying our throughput. Locking at the product level ensures we have maximum concurrency while guaranteeing
absolute thread safety for specific high-demand items." */
class Warehouse {
private String warehouseId;
private Location location;
private Map<String, Queue<Item>> inventory = new ConcurrentHashMap<>();
private Map<String, ReentrantLock> productLocks = new ConcurrentHashMap<>();
public Warehouse(String warehouseId, Location location) {
this.warehouseId = warehouseId;
this.location = location;
}
public String getWarehouseId() { return warehouseId; }
public void restockItem(Item item) {
inventory.putIfAbsent(item.getProductId(), new ConcurrentLinkedQueue<>());
inventory.get(item.getProductId()).offer(item);
item.setState(ItemState.AVAILABLE);
}
public List<Item> reserveItems(String productId, int quantity) throws Exception {
ReentrantLock lock = productLocks.computeIfAbsent(productId, k -> new ReentrantLock());
lock.lock(); // Critical Section Begins
try {
Queue<Item> availableItems = inventory.getOrDefault(productId, new ConcurrentLinkedQueue<>());
if (availableItems.size() < quantity) {
throw new Exception("Insufficient stock in Warehouse " + warehouseId);
}
List<Item> pickedItems = new ArrayList<>();
for (int i = 0; i < quantity; i++) {
Item item = availableItems.poll(); // O(1) thread-safe removal
if (item != null) {
item.setState(ItemState.RESERVED);
pickedItems.add(item);
}
}
return pickedItems;
} finally {
lock.unlock(); // Critical Section Ends - ALWAYS in a finally block
}
}
public int getAvailableCount(String productId) {
return inventory.containsKey(productId) ? inventory.get(productId).size() : 0;
}
}
// ==========================================
// 3. DESIGN PATTERNS (Strategy & Observer)
// ==========================================
/* INTERVIEW BONUS POINT: "By using the Strategy Pattern for warehouse selection, our system is highly extensible.
Right now, I am implementing a 'NearestWarehouseStrategy' to save on shipping costs. But if the business requirements
change—say, they want to fulfill orders from warehouses with the most stagnant inventory to clear space—we just
add a new Strategy class without touching the core InventoryManager logic. This adheres beautifully to the
Open/Closed Principle." */
interface WarehouseSelectionStrategy {
Warehouse selectWarehouse(List<Warehouse> warehouses, String productId, int quantity, Location customerLocation);
}
class NearestWarehouseStrategy implements WarehouseSelectionStrategy {
@Override
public Warehouse selectWarehouse(List<Warehouse> warehouses, String productId, int quantity, Location customerLocation) {
for (Warehouse w : warehouses) {
if (w.getAvailableCount(productId) >= quantity) {
return w;
}
}
return null;
}
}
/* INTERVIEW EXPLANATION: "For the low-stock alerts, I implemented the Observer Pattern.
The core inventory system shouldn't care *who* needs to know about low stock (e.g., Procurement service,
Slack bot, Email service). It just broadcasts the event. This decouples the inventory domain from notification domains." */
interface InventoryObserver {
void onLowStock(String productId, int remainingCount, String warehouseId);
}
class ProcurementService implements InventoryObserver {
@Override
public void onLowStock(String productId, int remainingCount, String warehouseId) {
System.out.println(">>> ALERT [Procurement]: Product " + productId +
" dropping! Only " + remainingCount + " left in " + warehouseId);
}
}
// ==========================================
// 4. CENTRAL MANAGER (Singleton)
// ==========================================
class InventoryManager {
private static final InventoryManager INSTANCE = new InventoryManager();
private List<Warehouse> warehouses = new CopyOnWriteArrayList<>();
private WarehouseSelectionStrategy routingStrategy;
private List<InventoryObserver> observers = new CopyOnWriteArrayList<>();
private static final int LOW_STOCK_THRESHOLD = 5;
private InventoryManager() {
this.routingStrategy = new NearestWarehouseStrategy();
}
public static InventoryManager getInstance() { return INSTANCE; }
public void addWarehouse(Warehouse w) { warehouses.add(w); }
public void addObserver(InventoryObserver o) { observers.add(o); }
public void setRoutingStrategy(WarehouseSelectionStrategy strategy) { this.routingStrategy = strategy; }
public List<Item> fulfillOrder(String productId, int quantity, Location customerLocation, String customerId) {
try {
Warehouse selectedWarehouse = routingStrategy.selectWarehouse(warehouses, productId, quantity, customerLocation);
if (selectedWarehouse == null) {
System.out.println("FAIL [" + customerId + "]: Out of stock for " + productId);
return Collections.emptyList();
}
List<Item> pickedItems = selectedWarehouse.reserveItems(productId, quantity);
System.out.println("SUCCESS [" + customerId + "]: Reserved " + quantity + " units from " + selectedWarehouse.getWarehouseId());
// Trigger Observers if necessary
int remaining = selectedWarehouse.getAvailableCount(productId);
if (remaining <= LOW_STOCK_THRESHOLD) {
notifyObservers(productId, remaining, selectedWarehouse.getWarehouseId());
}
return pickedItems;
} catch (Exception e) {
System.out.println("ERROR [" + customerId + "]: " + e.getMessage());
return Collections.emptyList();
}
}
private void notifyObservers(String productId, int remaining, String warehouseId) {
for (InventoryObserver observer : observers) {
observer.onLowStock(productId, remaining, warehouseId);
}
}
}
// ==========================================
// 5. MAIN SIMULATION
// ==========================================
public class InventoryManagementSystemDemo {
public static void main(String[] args) throws InterruptedException {
InventoryManager manager = InventoryManager.getInstance();
manager.addObserver(new ProcurementService());
Warehouse w1 = new Warehouse("WH-NewYork", new Location(40.71, -74.00));
manager.addWarehouse(w1);
Product laptop = new Product("PROD-100", "High-End Laptop", 1999.99);
System.out.println("--- Restocking Warehouse ---");
for (int i = 1; i <= 10; i++) {
w1.restockItem(new Item("BARCODE-" + i, laptop.getProductId(), "Aisle-1"));
}
System.out.println("Initial Stock in NY: " + w1.getAvailableCount(laptop.getProductId()) + "\n");
System.out.println("--- Flash Sale Begins! 15 customers trying to buy ---");
ExecutorService executor = Executors.newFixedThreadPool(15);
Location customerLoc = new Location(40.00, -73.00);
for (int i = 1; i <= 15; i++) {
final String customerId = "Customer-" + i;
executor.submit(() -> manager.fulfillOrder(laptop.getProductId(), 1, customerLoc, customerId));
}
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("\n--- Flash Sale Ends ---");
System.out.println("Final Stock in NY: " + w1.getAvailableCount(laptop.getProductId()));
}
}If you finish early, the interviewer will likely ask you how to scale this further. Be ready to discuss:
-
Order Splitting: If a user wants 5 items, and WH-A has 3, and WH-B has 2, our current
NearestWarehouseStrategyfails. You'd explain that you would update the Strategy to return aMap<Warehouse, Integer>outlining the split shipment. -
Cart TTL (Time-To-Live): Explain that
ItemState.RESERVEDimplies the item is in a cart. You would introduce a background cron job (or use a Redis TTL in a real distributed system) that sweeps the database every minute, finding items stuck inRESERVEDfor$> 10$ minutes, and resetting them toAVAILABLE. -
FEFO (First-Expiring, First-Out): If the prompt changes to a grocery store, you would swap the
ConcurrentLinkedQueuefor aPriorityQueue, sorting items by their expiration date.
While our core engine safely handles high-concurrency flash sales via fine-grained locking, a production-level interview might ask how we handle complex object creation or distributed server environments. Here are the precise enhancements to discuss:
Currently, our Product class is relatively simple. If we were to expand this system to handle highly complex items (like Electronics with optional warranties, varying power consumption, or specific wireless connectivities), we should integrate the following patterns:
-
The Builder Pattern: Instead of having constructors with 10+ parameters or relying on heavy setter methods, we would use the Builder Pattern. It is perfect for creating complex product objects with many optional parameters. It guarantees that once the
Productis built, it is immutable and thread-safe. -
The Factory Pattern: To keep the
InventoryManagerclean, we would encapsulate the logic of object instantiation. AProductFactoryclass would be responsible for returning the correctProductinstance based on a providedProductCategoryenum.
🎤 Interview Script for Creational Patterns:
"To keep our core engine focused on thread-safe transactions, I would extract the object creation logic out to a
ProductFactory. Furthermore, since products like Electronics have many optional attributes, I would implement the Builder Pattern. This ensures clean, step-by-step construction of theProductobjects without polluting the codebase with massive constructors."
Many standard tutorials implement a deep inheritance tree where ElectronicsProduct, ClothingProduct, and GroceryProduct all heavily extend an abstract Product base class.
- The Trade-off: Deep inheritance trees make the domain brittle. If a product belongs to two categories (e.g., an electronic wearable shirt), strict inheritance breaks down.
- The Solution: We favor Composition and Tagging. Instead of extending classes, we give a base
Productclass a list ofCategorytags or anAttributesmap. This is much more flexible for a real-world e-commerce catalog.
To deploy our Warehouse logic across thousands of servers, we would abstract our in-memory data structures:
- Repository Layer (Database Abstraction): We would abstract the
ConcurrentHashMapbehind anInventoryRepositoryinterface. This allows us to easily swap our in-memory map for a persistent SQL database or a Redis cache without altering the core routing logic. - Distributed Locking: Our
ReentrantLockguarantees safety for threads running on a single server. In a distributed environment, we would replace this with a distributed locking mechanism like Redis Redlock or Apache Zookeeper to synchronize locks across multiple JVMs. - Idempotency Keys: If a user's network drops during checkout and their app automatically retries the request, we risk reserving a second item. We would require an
idempotencyKey(e.g., a checkout session UUID) in thefulfillOrderparameters to ensure the operation is strictly processed only once. - The Saga Pattern & Cart TTL: If we successfully reserve items (transitioning them to
RESERVED), but the external Payment Service fails a few seconds later, we need a rollback strategy. A background cron job (or Redis TTL) would sweep the database every minute, identify items stuck inRESERVEDfor more than 10 minutes, and revert them toAVAILABLEto prevent inventory leakage.