Capstone: A Library System
An end-to-end modeling exercise that uses encapsulation, composition, interfaces, polymorphism, and responsibility-driven design
This capstone applies every idea from the course to one system: a small library management system. Read the description, work through the modeling steps with us, then write the missing pieces.
The story
A library has a catalog of books. People can become members of the library. Members may borrow books for a limited time. The library tracks every loan and can list which books are currently out and to whom. Different libraries enforce different borrowing policies (e.g. how many books one person may hold at once).
That paragraph contains everything we need.
Step 1 — find the objects
Underline the nouns. The candidates are:
Library— coordinates everythingBook— a thing on a shelfMember— a person registered with the libraryLoan— a fact: this book is out to this member, until this dateBorrowingPolicy— a rule: who may borrow how much
Loans and policies are subtle and easy to miss. They're not nouns in
the strictest sense, but they're first-class concepts in the
domain, so they deserve their own classes. (If you reduced them to
fields on Book or methods on Library, the design would get
muddier.)
Step 2 — assign responsibilities
Using the cards format from Responsibility-Driven Design:
| Class | Responsibilities | Collaborators |
|---|---|---|
Book | Know its title, author, and ISBN | — |
Member | Know its name and id; know its current loans; count them | Loan |
Loan | Know the borrowed book, the member, and the due date; know if overdue | Book, Member |
BorrowingPolicy | Decide if a given member may borrow another book | Member |
Library | Hold the catalog and members; create and track loans; enforce the policy | all of the above |
Step 3 — sketch the relationships
Two interesting design choices in that diagram:
BorrowingPolicyis an interface. Different libraries can plug in different rules without touchingLibrary. This is programming to an interface (and the Strategy pattern again).Loanis a first-class object that aggregates a book and a member. TheMemberalso keeps a list of its own loans — bidirectional access — butLoanitself owns the due date and the "is overdue?" decision.
Step 4 — message flow for borrowing
The conversation that happens when a member asks to borrow a book:
Library does not implement the policy itself. It asks. The
policy in turn asks the member how many loans they have. Nobody
reaches into anyone's fields.
Step 5 — build it
Most of the code is provided. You will finish two pieces:
Loan.isOverdue(int nowDay)— return whether the current day is strictly after the loan's due day.LimitPolicy.mayBorrow(Member m)— return whether the member's current loan count is strictly less than the limit.
Read Library.borrow(...) carefully — it's the orchestration in
miniature.
Step 6 — read your own design
If you completed the exercise, take a moment to look at what you just built. Notice that:
- Every class has a small, focused responsibility. None of them exceeded 30 or 40 lines.
Libraryis the orchestrator. It owns very little business logic; it sends messages.BorrowingPolicyis a plug-in. ReplacingLimitPolicy(2)inMainwithnew OpenPolicy()changes the library's behavior without changing a single line ofLibrary.Loanis a first-class object. The "is overdue?" decision lives where the data lives.- Nothing reaches into anyone else's fields. Every interaction is a message.
That is what a pure-OOP model looks like. The exact same shape
scales to a real library system with thousands of books and
hundreds of members, simply by changing the storage of books,
members, and active from in-memory lists to a database — and
because that storage is hidden behind Library's methods, no other
class would notice.
Stretch ideas (optional)
If you want to keep going, try any of these:
- Add a
Reservationclass: a member can reserve a book that's currently checked out and be next in line when it returns. - Add a
WeekendPolicythat doubles the allowed loan count on weekends, demonstrating polymorphic policy composition. - Replace the
int today/loanDayswith ajava.time.LocalDateand pass an injectedClockintoLibrary. This is a real-world pattern for testability. - Introduce a
Catalogclass that owns the book list and anAuditclass that records every borrow and return — splittingLibrary's remaining work along its natural seams.
Each of those is a textbook responsibility-driven extension.
Test your understanding
Why is BorrowingPolicy declared as an interface in this design?
Because Java requires every Library to use one
So that different libraries can plug in different policies without modifying Library
Because Java forbids classes from holding boolean methods
To save memory
Why does the Loan class own the isOverdue(...) method instead of Library owning it?
Library is too small to hold any methods
Java forbids Library from having boolean methods
The relevant data (the due date) lives on Loan, so the behavior belongs there too — that's "tell, don't ask"
To make Loan extend Library
If we wanted to add a "weekend libraries allow more loans" rule, the cleanest change in this design is:
Add a giant if in Library.borrow that checks the day of the week
Add a new field to Member
Write a new class WeekendPolicy implements BorrowingPolicy and pass it into Library's constructor — Library itself does not change
Modify LimitPolicy to know about weekends