Low-Level Design: Library Management System — Catalog, Borrowing, Reservations, and Fines

Updated · techinterview.org

Core Entities

Book: book_id, isbn, title, authors (array), publisher, published_year, genre, language, description, cover_url. BookCopy: copy_id, book_id, library_id, condition (NEW, GOOD, FAIR, POOR), status (AVAILABLE, BORROWED, RESERVED, LOST, DAMAGED), acquisition_date, last_audited_at. Member: member_id, first_name, last_name, email, phone, address, type (STUDENT, FACULTY, PUBLIC, STAFF), membership_expiry, borrow_limit, status (ACTIVE, SUSPENDED, EXPIRED). Loan: loan_id, copy_id, member_id, borrowed_at, due_date, returned_at, renewal_count, fine_amount, fine_paid. Reservation: reservation_id, book_id, member_id, status (PENDING, READY, FULFILLED, EXPIRED, CANCELLED), requested_at, ready_at, expires_at. Fine: fine_id, loan_id, member_id, amount, reason (OVERDUE, DAMAGED, LOST), issued_at, paid_at, waived_by.

Borrowing and Return Flow

class LibraryService:
    LOAN_PERIODS = {'STUDENT': 14, 'FACULTY': 30, 'PUBLIC': 14, 'STAFF': 21}
    DAILY_FINE_RATE = 0.25  # $0.25 per day overdue
    MAX_RENEWALS = 2

    def borrow_book(self, member_id: int, copy_id: int) -> Loan:
        with self.db.transaction():
            member = self.repo.get_member(member_id, lock=True)
            if member.status != MemberStatus.ACTIVE:
                raise InactiveMemberError()
            if member.membership_expiry = member.borrow_limit:
                raise BorrowLimitExceededError(
                    f'Limit: {member.borrow_limit}, active: {active_loans}'
                )

            outstanding_fine = self.repo.get_outstanding_fine_total(member_id)
            if outstanding_fine > 10.00:
                raise OutstandingFinesError(f'Unpaid fines: ${outstanding_fine:.2f}')

            copy = self.repo.get_copy(copy_id, lock=True)
            if copy.status != CopyStatus.AVAILABLE:
                raise CopyNotAvailableError()

            due_date = date.today() + timedelta(days=self.LOAN_PERIODS[member.type])
            loan = Loan(copy_id=copy_id, member_id=member_id,
                        borrowed_at=datetime.utcnow(), due_date=due_date,
                        renewal_count=0)
            copy.status = CopyStatus.BORROWED
            self.repo.save(loan)
            self.repo.save(copy)
            return loan

    def return_book(self, loan_id: int) -> Loan:
        with self.db.transaction():
            loan = self.repo.get_loan(loan_id, lock=True)
            loan.returned_at = datetime.utcnow()
            copy = self.repo.get_copy(loan.copy_id)

            if loan.returned_at.date() > loan.due_date:
                overdue_days = (loan.returned_at.date() - loan.due_date).days
                loan.fine_amount = overdue_days * self.DAILY_FINE_RATE
                self.repo.create_fine(loan.loan_id, loan.member_id,
                                      loan.fine_amount, 'OVERDUE')

            # Check if anyone has a reservation for this book
            reservation = self.repo.get_next_reservation(copy.book_id)
            if reservation:
                copy.status = CopyStatus.RESERVED
                reservation.status = ReservationStatus.READY
                reservation.ready_at = datetime.utcnow()
                reservation.expires_at = datetime.utcnow() + timedelta(days=3)
                self.repo.save(reservation)
                self._notify_member(reservation.member_id, 'BOOK_READY')
            else:
                copy.status = CopyStatus.AVAILABLE
            return loan

Catalog Search and Discovery

Search requirements: full-text search across title, author, ISBN, subject. Filter by availability, genre, language, year range. Elasticsearch index on the Book table. Index fields: title (analyzed, boosted 2x), authors (analyzed), isbn (keyword), genre (keyword), language (keyword), published_year (integer). On book added/updated: sync to Elasticsearch via event-driven update (or near-real-time sync using Debezium CDC from PostgreSQL). Search query: multi-match across title and authors. Filter by genre and language. Sort by relevance (default), title (alphabetical), or year. Availability filter: a “has_available_copy” boolean on the Book index, updated when copy statuses change. Note: this is denormalized — updated asynchronously, may be slightly stale. For exact availability: query the BookCopy table directly. Autocomplete: edge n-gram analyzer on title and author fields for prefix-based suggestions as the user types. Return top 10 suggestions with book cover thumbnail URL. ISBN lookup: exact match, bypasses full-text search entirely.

Renewals, Reservations, and Reporting

Renewal: extend due_date by the standard loan period. Restrictions: cannot renew if the book has pending reservations (another member is waiting). Cannot renew if already renewed MAX_RENEWALS times. Cannot renew if the member has outstanding fines > $10. Each renewal increments renewal_count. Reservation flow: if no copies are available, member creates a reservation. Reservations are fulfilled FIFO per book. On copy return: system checks the reservation queue and assigns the copy to the next reservation. Member has 3 days to pick up before the reservation expires and the next in queue is notified. Automated jobs: OverdueReminderJob: daily, query loans where due_date = today + 2 days and not returned. Send reminder email. OverdueFineJob: daily, query loans where due_date < today and not returned. Compute accruing fine (daily_rate * overdue_days). Reservation expiry job: hourly, query reservations where status=READY and expires_at < now. Mark EXPIRED, release copy, notify next in queue. Reporting: books borrowed per month, most popular titles, overdue by member, fine revenue by category. Generated via SQL aggregates on the Loan and Fine tables. Exported to CSV or displayed in the admin dashboard.

See also: Shopify Interview Prep

See also: LinkedIn Interview Prep

See also: Airbnb Interview Prep

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

1972 Soviet postage stamp commemorating the Mars 2 probe

worth a read

Mars For The Rest of Us — a weekly-or-more deep dive on the technical side of Mars exploration: rocket propulsion, microbiology, mission architecture, and everything in between. Written by Maciej Ceglowski.

Read it on Substack
Scroll to Top