Designing a library management system
In this series (15 parts)
- Introduction to low level design
- SOLID principles
- Design patterns: Creational
- Design patterns: Structural
- Design patterns: Behavioral
- Designing a parking lot
- Designing a library management system
- Designing an elevator system
- Designing a hotel booking system
- Designing a ride-sharing model
- Designing a rate limiter
- Designing a logging framework
- Designing a notification system
- API design and contract-first development
- Data modeling for system design
A library management system is a staple LLD problem because it combines inventory tracking, user role management, state machines, and business rules like fines and reservations. The domain is familiar to everyone, so you can focus on the design rather than explaining what a library does.
Requirements
Functional:
- The library holds books. Each book can have multiple physical copies (book items).
- Members can search for books by title, author, or ISBN.
- Members can check out up to 5 book items at a time for a maximum of 14 days.
- Members can reserve a book item that is currently checked out.
- Librarians can add, update, and remove books and book items.
- The system charges a daily fine for overdue returns.
- A member with outstanding fines cannot check out new books.
Non-functional:
- The book item lifecycle must be explicit and auditable (state machine).
- Search should support partial matches.
Entities
From the requirements, the core nouns are: Book, BookItem, Member, Librarian, Loan, Reservation, Fine. Notice the distinction between Book (a title, like “Clean Code”) and BookItem (a specific physical copy with a barcode).
Class diagram
classDiagram
class Book {
-String isbn
-String title
-String author
-String publisher
-int year
-List~BookItem~ copies
+addCopy(BookItem)
+removeCopy(String barcode)
}
class BookItem {
-String barcode
-Book book
-BookItemStatus status
-LocalDate dueDate
+checkout(Member) Loan
+returnItem() void
+markLost() void
}
class Account {
<<abstract>>
-String id
-String name
-String email
-AccountStatus status
}
class Member {
-List~Loan~ activeLoans
-List~Reservation~ reservations
-double outstandingFines
+canCheckout() boolean
+checkout(BookItem) Loan
+returnBook(BookItem) void
+reserve(Book) Reservation
}
class Librarian {
+addBook(Book) void
+addBookItem(BookItem) void
+removeBookItem(String barcode) void
}
class Loan {
-String loanId
-BookItem item
-Member member
-LocalDate issueDate
-LocalDate dueDate
-LocalDate returnDate
-LoanStatus status
+isOverdue() boolean
+closeLoan(LocalDate) void
}
class Reservation {
-String reservationId
-Book book
-Member member
-LocalDate reservationDate
-ReservationStatus status
+fulfill(BookItem) Loan
+cancel() void
}
class Fine {
-String fineId
-Loan loan
-double amount
-FineStatus status
+calculate(double dailyRate) double
+pay() void
}
class BookItemStatus {
<<enumeration>>
AVAILABLE
CHECKED_OUT
RESERVED
LOST
RETIRED
}
Account <|-- Member
Account <|-- Librarian
Book "1" *-- "many" BookItem
Member "1" o-- "many" Loan
Member "1" o-- "many" Reservation
Loan "1" --> "1" BookItem
Loan "1" --> "1" Member
Reservation "1" --> "1" Book
Fine "1" --> "1" Loan
BookItem --> BookItemStatus
Class diagram for the library management system.
Book item state machine
The lifecycle of a physical book copy is the most important state machine in this system. Getting the transitions right prevents bugs like checking out a book that is already loaned or reserving a retired copy.
stateDiagram-v2 [*] --> Available Available --> CheckedOut : checkout() Available --> Reserved : reserve() Available --> Retired : retire() Reserved --> CheckedOut : fulfillReservation() Reserved --> Available : cancelReservation() CheckedOut --> Available : returnItem() CheckedOut --> Lost : reportLost() Lost --> Available : itemFound() Lost --> Retired : writeOff() Retired --> [*]
State diagram for a book item’s lifecycle.
Key rules encoded in the transitions:
- You cannot check out a
Reserveditem directly. The reservation must be fulfilled first. - A
Lostitem can be recovered (itemFound) or permanently written off. Retiredis a terminal state. The physical copy is removed from circulation.
Implement this with an enum and guard methods:
public void checkout(Member member) {
if (status != BookItemStatus.AVAILABLE) {
throw new IllegalStateException("Cannot checkout item in state: " + status);
}
this.status = BookItemStatus.CHECKED_OUT;
this.dueDate = LocalDate.now().plusDays(14);
}
Checkout flow
- The member calls
checkout(bookItem). - The system verifies
member.canCheckout(): fewer than 5 active loans and zero outstanding fines. - The system verifies the book item is
AVAILABLE. - A new
Loanis created with a 14-day due date. - The book item status moves to
CHECKED_OUT.
public Loan checkout(BookItem item) {
if (!canCheckout()) {
throw new CheckoutDeniedException("Member cannot checkout: limit or fines");
}
Loan loan = item.checkout(this);
activeLoans.add(loan);
return loan;
}
public boolean canCheckout() {
return activeLoans.size() < MAX_LOANS && outstandingFines == 0;
}
Reservation flow
When a member wants a book but all copies are checked out:
- Member calls
reserve(book). - The system creates a
Reservationwith statusPENDING. - When any copy of that book is returned, the system checks the reservation queue.
- The first reservation in the queue is fulfilled: the book item moves to
RESERVEDand the member is notified. - If the member does not pick up within 3 days, the reservation is cancelled automatically.
This is a classic Observer pattern use case. The return event triggers notification to the reservation subsystem.
Fine calculation
Fines are calculated on return, not continuously. The formula is simple:
overdueDays = returnDate - dueDate
fine = overdueDays * dailyRate
public class FineCalculator {
private final double dailyRate;
public FineCalculator(double dailyRate) {
this.dailyRate = dailyRate;
}
public double calculate(Loan loan) {
if (!loan.isOverdue()) return 0.0;
long overdueDays = ChronoUnit.DAYS.between(loan.getDueDate(), loan.getReturnDate());
return overdueDays * dailyRate;
}
}
If a book is reported lost, charge the replacement cost instead. This can be a separate strategy behind the same FineCalculator interface.
Search
Search is where a naive design gets messy fast. Keep a dedicated SearchService with an index, rather than looping through every book on each query.
public interface SearchService {
List<Book> searchByTitle(String title);
List<Book> searchByAuthor(String author);
Book searchByISBN(String isbn);
}
For an in-memory implementation, maintain hash maps keyed by title words, author name, and ISBN. For a production system, back this with a full-text search engine like Elasticsearch.
The important design point: the SearchService is a separate concern from Book or Library. Do not put search logic inside the Book class.
Handling concurrent checkouts
Two members trying to check out the last available copy of a book at the same time is the equivalent of the parking lot’s spot allocation race. Apply the same compare-and-set pattern:
public synchronized Loan checkout(Member member) {
if (status != BookItemStatus.AVAILABLE) {
throw new IllegalStateException("Item no longer available");
}
this.status = BookItemStatus.CHECKED_OUT;
// ... create loan
}
Or use an optimistic locking approach at the database level with a version column on book_item.
Extensibility
| Change | Impact |
|---|---|
| Add audiobooks | New BookItem subclass, no change to Loan |
| Add renewal (extend loan) | New method on Loan, new transition in state |
| Add waitlist priority | Priority queue in ReservationService |
| Add notification system | Observer on return event, new Notifier class |
What comes next
The elevator system takes state machines further. Instead of a simple status enum, the elevator has continuous state (current floor, direction, request queue) and requires a scheduling algorithm to decide where to go next.