-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathInventoryManagementSystemLLD.java
More file actions
242 lines (201 loc) · 10.1 KB
/
InventoryManagementSystemLLD.java
File metadata and controls
242 lines (201 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
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 InventoryManagementSystemLLD {
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()));
}
}