Search…
Low Level Design · Part 7

Designing a library management system

In this series (15 parts)
  1. Introduction to low level design
  2. SOLID principles
  3. Design patterns: Creational
  4. Design patterns: Structural
  5. Design patterns: Behavioral
  6. Designing a parking lot
  7. Designing a library management system
  8. Designing an elevator system
  9. Designing a hotel booking system
  10. Designing a ride-sharing model
  11. Designing a rate limiter
  12. Designing a logging framework
  13. Designing a notification system
  14. API design and contract-first development
  15. 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:

  1. The library holds books. Each book can have multiple physical copies (book items).
  2. Members can search for books by title, author, or ISBN.
  3. Members can check out up to 5 book items at a time for a maximum of 14 days.
  4. Members can reserve a book item that is currently checked out.
  5. Librarians can add, update, and remove books and book items.
  6. The system charges a daily fine for overdue returns.
  7. 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 Reserved item directly. The reservation must be fulfilled first.
  • A Lost item can be recovered (itemFound) or permanently written off.
  • Retired is 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

  1. The member calls checkout(bookItem).
  2. The system verifies member.canCheckout(): fewer than 5 active loans and zero outstanding fines.
  3. The system verifies the book item is AVAILABLE.
  4. A new Loan is created with a 14-day due date.
  5. 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:

  1. Member calls reserve(book).
  2. The system creates a Reservation with status PENDING.
  3. When any copy of that book is returned, the system checks the reservation queue.
  4. The first reservation in the queue is fulfilled: the book item moves to RESERVED and the member is notified.
  5. 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 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

ChangeImpact
Add audiobooksNew BookItem subclass, no change to Loan
Add renewal (extend loan)New method on Loan, new transition in state
Add waitlist priorityPriority queue in ReservationService
Add notification systemObserver 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.

Start typing to search across all content
navigate Enter open Esc close