Guide / build route
Inventory management program in Java
Java is a fine choice for an inventory program: typed models, real concurrency primitives, and a database ecosystem on tap. The design that makes it work — movements, not totals — and the honest assessment of what a homemade program costs once it works.
The key facts
- The core classes: Product (with variants), Location, StockMovement (append-only, signed quantity), Inventory (deriving on-hand), Order and OrderLine — mapped to tables via JDBC or JPA.
- The one rule: on-hand is computed from movements, never a mutable field. A
setQuantityOnHand()method is a design bug, not a convenience. - The Java specifics: transactions via JDBC or Spring,
SELECT FOR UPDATEor atomic updates for the last-unit race,BigDecimalfor quantities and money — neverdouble. - The honest exit: the program teaches the domain; the maintained record is what a business runs — the build-vs-buy arithmetic prices both.
- The wider system: inventory management end to end maps the whole record this page sits in.
- 01The key facts
- 02The classes that matter
- 03The Java specifics that bite
- 04The scope, honestly staged
- 05The exit that keeps the learning
The classes that matter
Product / ProductVariant — identity, unit, cost; variants for size and pack. Location — where stock sits. StockMovement — the heart: variant, location, signed quantity, timestamp, actor, reason, reference. Append-only by convention: no setter ever mutates a movement; corrections are new movements. Inventory — not a stored balance but a query: SELECT SUM(signed_quantity) ... GROUP BY variant, location, or a rebuildable projection. Order / OrderLine — stateful records with a copied-at-order-time price, moving through a state machine (received → allocated → picked → invoiced) enforced in one service class.
The Java-ness is in the discipline the type system enables: make StockMovement immutable, expose no mutators for derived values, and let the compiler help you keep the invariants you wrote down. The database design pairs with this class model table for table.
The Java specifics that bite
Money and quantities are BigDecimal, with explicit scale and rounding — double turns cents into folklore. The last-unit race is solved in the database, not the JVM: SELECT ... FOR UPDATE on the affected rows, or an atomic conditional insert for the reservation, so two threads racing resolve to one sale and one clean rejection. Transactions wrap the invariant — allocate-plus-reserve, cancel-plus-release — committed or rolled back as a unit. Persistence starts with plain JDBC or SQLite for learning and moves to Postgres with a connection pool for anything shared; JPA works, but map movements as immutable entities and resist the convenience of a cached quantityOnHand field.
Console first, then a thin web layer if the program earns one: the domain logic should never know about the interface, because the interface will change and the invariants will not.
The scope, honestly staged
A console program over one table file — a weekend. A JDBC program with the movement core, reorder reporting and a stocktake-correction flow — a few weekends, and genuinely useful for learning the domain. A multi-user program with an order state machine, purchasing and an invoice handoff — months, plus the permanent tail: hosting, security patches, backups, and every future requirement. That tail is the real cost of the Java route, and it is paid in the hours of whoever owns the program. The worked example shows the domain behaviour the program should reproduce.
The exit that keeps the learning
We build BSimple, so weigh that: it runs this exact domain model — movements, reservations, deliberate invoice approval, per-customer pricing — as maintained cloud software, from $180/month AUD. (Its own implementation stack is not published, and should not influence your Java choice anyway.) The exit is not a loss: the classes you designed are the specification, and the public REST API with scoped keys and read projections means your Java program can become a client of a maintained record instead of its lonely owner — the trial shows the record it would talk to.
Frequently Asked Questions
Is Java a good language for an inventory program?
Yes — strong typing, mature database access, and real concurrency primitives suit the domain. The language matters far less than the design: append-only movements and derived quantities, whatever the stack.
Should I use JPA/Hibernate or plain JDBC?
Either works; plain JDBC teaches more and hides less for a learning project. With JPA, map movements as immutable and avoid cached balance fields — the ORM should not tempt you into storing what must be derived.
How do I handle two users selling the last item?
In the database: row-level locking (SELECT ... FOR UPDATE) or an atomic conditional reservation insert, inside a transaction. Application-level check-then-write logic loses races under load, however tidy the Java.
What type should I use for quantities and money?
BigDecimal with explicit scale for both — never double or float. Inventory and billing errors from binary floating point are silent, small, and exactly the kind that destroy trust in a record.
Can my Java program connect to a maintained system instead?
Yes — BSimple's public REST API (scoped keys, read projections for reorder levels, stocktakes, purchase orders and suppliers) lets a Java client render or extend a maintained record; the trial shows the data it would read.
BSimple

