An in-memory stock ledger owns item quantities and accepts changes only when each resulting balance remains within its declared range.
Java project: an in-memory stock ledger with validated mutations
Java 8+. Use a JDK that supports this release.
Define the project boundaries
The ledger supports receive, dispatch, and read-only snapshot operations. Item IDs must be nonblank, quantities must be positive, dispatch cannot create a negative balance, and arithmetic overflow rejects the operation.
This is a runnable single-process project. It contains no database, authentication, concurrent writers, or event history. Restarting the program loses the ledger. Those limits belong in the project definition before anyone depends on it.
Use a TreeMap for predictable item ordering in the snapshot. That is a display contract, not a requirement that every stock system use sorted storage.
Reject before changing state
receive computes the checked new quantity before put. dispatch checks the current quantity before subtracting. A failed mutation leaves the existing balance intact.
The snapshot copies the map and then wraps the copy as unmodifiable. Returning a wrapper around the original map would expose later internal changes to an earlier reader. Integers and Strings keep this snapshot structurally and element-wise safe for this use.
Acceptance checks and next extensions
Run the program to receive ten units, dispatch three, attempt an excessive dispatch, and print the unchanged balance after rejection. Then add tests for unknown items, zero units, blank IDs, and overflow.
A persistence extension should commit changes transactionally. A concurrent extension needs one owner or an atomic operation across the balance rule. Adding a concurrent map alone does not make read-check-write atomic.
Working program
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
public class StockLedgerProject {
static final class Ledger {
private final Map<String, Integer> quantities = new TreeMap<>();
private void validate(String item, int units) {
if (item == null || item.trim().isEmpty() || units <= 0) {
throw new IllegalArgumentException("Item and positive units required");
}
}
void receive(String item, int units) {
validate(item, units);
int updated = Math.addExact(quantities.getOrDefault(item, 0), units);
quantities.put(item, updated);
}
void dispatch(String item, int units) {
validate(item, units);
int available = quantities.getOrDefault(item, 0);
if (units > available) throw new IllegalArgumentException("Insufficient stock");
quantities.put(item, available - units);
}
Map<String, Integer> snapshot() {
return Collections.unmodifiableMap(new TreeMap<>(quantities));
}
}
public static void main(String[] args) {
Ledger ledger = new Ledger();
ledger.receive("packing-tape", 10); ledger.dispatch("packing-tape", 3);
Map<String, Integer> saved = ledger.snapshot();
try { ledger.dispatch("packing-tape", 8); }
catch (IllegalArgumentException expected) { System.out.println("rejected"); }
ledger.receive("packing-tape", 2);
System.out.println("saved=" + saved);
System.out.println("current=" + ledger.snapshot());
}
}Output
rejected
saved={packing-tape=7}
current={packing-tape=9}Cost and design choices
Each map lookup or update costs O(log k) comparisons for k items. The snapshot copies k entries and requires O(k) new storage. Text comparison costs depend on item ID length.
Snapshots have a real cost. A large high-frequency reader may need another publication design, but that change must preserve a defined ownership and consistency contract.
Common Mistakes
- Do not claim the ledger survives a restart.
- Do not return a live internal map as a snapshot.
- Do not use unsynchronized read-check-write with concurrent callers.
Connect the contracts
Review shared stock invariants before splitting the state across independently updated containers.
Compare the boundary explained in Boundary tests with the assumptions made by this program.
