Background
The Fulfillment Systems team at a large online retailer runs the service that promises stock to customer orders. When a customer checks out, the order service reserves units; when the warehouse ships, it commits them; when an order is cancelled, it releases them. Since the last deployment, checkouts are failing for items that are clearly in stock, and the order service is getting error codes it doesn't expect.
How the system works
Stock file
Stock is loaded from a CSV string, one sku,name,onHand row per line:
sku,name,onHand
KB-100,Mechanical keyboard,5
MS-200,Wireless mouse,12
- If the first non-blank line starts with
sku(any case), it is a header and is skipped. Blank lines are ignored, and every field is trimmed (so Windows\r\nline endings work). - Each row must have exactly 3 fields. The SKU must not be empty.
onHandmust be a whole number ≥ 0. A SKU may appear only once. Any violation throwsInvalidRequestException. - Loading a stock file replaces all stock and clears every reservation. Reservation ids restart at
R-0001.
SKUs
SKUs are case-insensitive and trimmed: " kb-100 ", "kb-100" and "KB-100" are the same SKU. SKUs are stored and returned in upper case.
Stock numbers
onHand: units physically in the building.reserved: units promised to orders that haven't shipped.available = onHand − reserved.
Reservations
| Operation | Rule |
|---|---|
| reserve(orderId, sku, quantity) | orderId must not be blank and quantity must be > 0 (else InvalidRequestException). Unknown SKU → NotFoundException. The reservation succeeds when quantity <= available (an order may take the very last units). Otherwise InsufficientStockException, and nothing changes. On success reserved grows by quantity, and a HELD reservation with the next id (R-0001, R-0002, …) is returned. |
| release(id) | HELD → RELEASED; reserved shrinks by the quantity. Releasing a RELEASED reservation again returns it unchanged. Releasing a COMMITTED one → IllegalTransitionException. |
| commit(id) | HELD → COMMITTED; both onHand and reserved shrink by the quantity. Committing a COMMITTED reservation again returns it unchanged. Committing a RELEASED one → IllegalTransitionException. |
| lowStock(threshold) | SKUs whose available is at or below threshold, sorted by available ascending, then SKU ascending. A negative threshold → InvalidRequestException. |
Unknown reservation ids → NotFoundException.
API
| Method + path | Success | Notes |
|---|---|---|
GET /api/stock/{sku} | 200 + {"sku","name","onHand","reserved","available"} | |
GET /api/stock/low?threshold=N | 200 + list of the same objects | threshold defaults to 5 |
POST /api/reservations with {"orderId","sku","quantity"} | 201 + {"id","orderId","sku","quantity","status"} | body is validated |
POST /api/reservations/{id}/release | 200 + reservation | |
POST /api/reservations/{id}/commit | 200 + reservation |
Errors return {"error": "<CODE>", "message": "<detail>"}:
| Exception | HTTP status | error |
|---|---|---|
NotFoundException | 404 | NOT_FOUND |
InsufficientStockException | 409 | INSUFFICIENT_STOCK |
IllegalTransitionException | 409 | INVALID_TRANSITION |
InvalidRequestException, or a request body failing validation | 400 | INVALID_REQUEST |
Project structure
src/main/java/com/bugnik/inventory/
InventoryApplication.java Spring Boot entry point
model/ StockItem, Reservation, ReservationStatus, StockView
repository/ StockRepository, ReservationRepository (in-memory)
service/ InventoryService (the rules above) + business exceptions
util/ Skus (normalization), StockCsvParser (stock file)
web/ InventoryController, ReserveRequest, ApiError, ApiExceptionHandler
src/test/java/... unit tests and MockMvc API tests
pom.xml Maven build (Spring Boot 3.3, Java 17)
Your task
The order service and the warehouse team have reported problems since the last release:
- The project no longer builds.
- Customers can't check out the last few units of an item, even though the stock page shows them as available.
- When an item really is out of stock, the order service receives a "bad request" error instead of a conflict, so it shows customers the wrong message.
Find and fix the defects so that all tests pass.
- Do not modify anything in
src/test/orpom.xml. - Keep your changes minimal. Fix the defects; don't rewrite modules.
- Additional hidden tests check the behaviour described in this README.