Library Management System
A layered Java console application for managing books, members, and loans; business logic lives entirely in the service layer, persistence is abstracted behind repository interfaces so JSON storage can be swapped for a database with zero service-layer changes, and every failure path raises a typed, predictable exception instead of a generic error.
Why I Built This
I wanted a project that forced strict layered architecture rather than a quick script, something where the business rules, persistence, and presentation genuinely couldn't leak into each other.
A library system is a good fit for that: books, members, and loans have enough real business rules (eligibility, due dates, fines) to make the layering actually matter, without needing a full web stack to prove the point.
Repository Pattern in Practice
ConsoleUI talks only to Services, Services talk only to Repository interfaces, and Repositories talk only to JSON storage, a strictly one-directional, interface-bound chain. Model, Utility, and Exception code cut across all three layers without breaking that direction. The practical payoff: storage can be swapped for a database later without the service or UI layer needing to change at all.
Members, Eligibility, and Fines
Membership status isn't just a flag; it's an input into whether a loan is even allowed to happen. The Issue Book flow checks a member's active status and current loan count before a book ever leaves the shelf, so an overdue member or one at their borrowing limit gets a clear rejection instead of a loan that quietly breaks the system's own rules later.
Fines are calculated automatically at return time based on how overdue a loan was, using the rate defined in the externalized properties file rather than a value baked into the code, so a library actually running this system could change its late fee without anyone touching a class.
Technical Challenges
Getting JSON persistence to be genuinely crash-safe took more than just calling a JSON library's write method, writes go to a temp file first and only get renamed into place on success, so a crash mid-write never leaves a half-written, corrupted file behind.
Real ISBN-10/13 checksum validation turned out to be more involved than expected, since it has to correctly handle both 10- and 13-digit formats and their different checksum algorithms rather than one shared rule.
Scalability Considerations
JSON files are the right choice for a console app with a handful of users, and the wrong choice past that, file-based storage doesn't handle concurrent writers safely, and read/write cost grows with file size rather than staying flat. That ceiling is exactly why the repository pattern mattered from day one: the service layer already talks to an interface, not a file, so swapping in something like PostgreSQL or SQLite later is a repository-layer change, not a rewrite.
The bigger scalability question a real deployment would raise is concurrency; this project assumes single-user, single-process access, and genuinely supporting multiple simultaneous librarians would mean adding locking or moving off file-based storage entirely, which is deliberately out of scope for what this project set out to prove.
Key Decisions
Persistence was built behind repository interfaces specifically so JSON storage could later be swapped for a real database without touching the service layer at all; the repository pattern was the whole point of the exercise, not an afterthought.
JSON writes are atomic (temp file → rename) rather than direct overwrites, so a crash mid-write can never leave the data file in a partially-written, corrupted state.
ISBN validation implements the real ISBN-10/13 checksum algorithm rather than just checking the string format, since format-only validation would happily accept an ISBN that looks right but is actually invalid.
Business rules like loan period and fine rate were externalized to a properties file rather than hardcoded, so the rules can change without a rebuild.
What I Learned
Enforcing that dependencies only ever flow downward (UI → Service → Repository → Storage) is easy to state and surprisingly easy to accidentally violate under real feature pressure; it took discipline to keep every new feature honoring that direction instead of taking a shortcut.
A typed, unchecked exception hierarchy made failure handling far more predictable across the whole app than generic exceptions would have, since every call site could reason about exactly what could go wrong.