Skip to main content
Difficulty: 🟢 Beginner | Time: 35-45 minutes | Patterns: Singleton, Observer, Factory

🎯 Problem Statement

Design a library management system that can:
  • Manage books, members, and librarians
  • Handle book borrowing and returns
  • Track due dates and calculate fines
  • Support book reservations
  • Search books by title, author, or ISBN
Why This Problem? Library is a great beginner problem that covers core OOP concepts - classes, relationships, and basic CRUD operations. Perfect for warming up before harder problems!

📋 Step 1: Clarify Requirements

Interview Tip: Library problems can be simple or complex. Clarify if you need multiple copies, reservations, and notifications.

Questions to Ask the Interviewer

CategoryQuestionImpact on Design
BooksMultiple copies of same book?BookCopy vs Book
MembersDifferent member types?Member hierarchy
LimitsMax books per member? Loan period?Validation logic
ReservationsCan reserve borrowed books?Reservation queue
FinesHow are fines calculated?Fine calculator
NotificationsDue date reminders?Observer pattern

Functional Requirements

  • Add/remove books from the library
  • Register new members
  • Borrow and return books
  • Reserve books that are currently borrowed
  • Search for books
  • Calculate and collect fines for overdue books
  • Send notifications for due dates

Non-Functional Requirements

  • Support multiple copies of the same book
  • Handle concurrent borrowing requests
  • Maintain borrowing history

🧩 Step 2: Identify Core Objects

Key Insight: Distinguish between Book (metadata - title, author, ISBN) and BookCopy (physical copy - barcode, status). One book can have many copies!

Books

Book, BookCopy, Rack Location

Users

Member, Librarian, Account

Transactions

Lending, Reservation, Fine

Entity-Responsibility Mapping

EntityResponsibilitiesPattern
LibraryManage catalog, coordinate operationsSingleton
BookStore metadata, track copies-
BookCopyTrack status, location-
MemberBorrow, return, pay fines-
LendingTrack borrow date, due date-
NotificationServiceDue date remindersObserver

Key Constraints

MAX_BOOKS_PER_MEMBER = 5
LOAN_PERIOD_DAYS = 14
FINE_PER_DAY = 0.50
MAX_RESERVATION_DAYS = 7

📐 Step 3: Class Diagram

┌─────────────────────────────────────────────────────────────┐
│                         Library                             │
├─────────────────────────────────────────────────────────────┤
│ - name: String                                              │
│ - books: Dict[ISBN, Book]                                   │
│ - members: Dict[MemberId, Member]                          │
├─────────────────────────────────────────────────────────────┤
│ + addBook(book): bool                                      │
│ + removeBook(isbn): bool                                   │
│ + registerMember(member): bool                             │
│ + searchByTitle(title): List<Book>                         │
│ + searchByAuthor(author): List<Book>                       │
└─────────────────────────────────────────────────────────────┘

                    ┌─────────┴─────────┐
                    │                   │
                    ▼                   ▼
┌───────────────────────────┐  ┌───────────────────────────┐
│          Book             │  │         Member            │
├───────────────────────────┤  ├───────────────────────────┤
│ - isbn: String            │  │ - id: String              │
│ - title: String           │  │ - name: String            │
│ - author: String          │  │ - email: String           │
│ - copies: List<BookCopy>  │  │ - borrowedBooks: List     │
├───────────────────────────┤  │ - reservations: List      │
│ + getAvailableCopy()      │  ├───────────────────────────┤
│ + addCopy(copy)           │  │ + borrow(book): Lending   │
└───────────────────────────┘  │ + return(lending): Fine?  │
            │                  │ + reserve(book): bool     │
            │ 1..*             └───────────────────────────┘

┌───────────────────────────┐
│        BookCopy           │
├───────────────────────────┤
│ - barcode: String         │
│ - status: CopyStatus      │
│ - rack: RackLocation      │
├───────────────────────────┤
│ + checkout(): bool        │
│ + checkin(): bool         │
└───────────────────────────┘

Step 4: Implementation

Enums and Constants

from enum import Enum
from datetime import datetime, timedelta
from typing import Optional, Dict, List
from dataclasses import dataclass, field
import uuid

class BookStatus(Enum):
    AVAILABLE = 1
    BORROWED = 2
    RESERVED = 3
    LOST = 4

class MemberStatus(Enum):
    ACTIVE = 1
    SUSPENDED = 2  # Too many overdue books
    CLOSED = 3

class ReservationStatus(Enum):
    PENDING = 1
    FULFILLED = 2
    CANCELLED = 3
    EXPIRED = 4

# Library constants
MAX_BOOKS_PER_MEMBER = 5
LOAN_PERIOD_DAYS = 14
FINE_PER_DAY = 0.50
MAX_RESERVATION_DAYS = 7

Book Classes

@dataclass
class RackLocation:
    floor: int
    section: str
    shelf: int

@dataclass
class Book:
    isbn: str
    title: str
    author: str
    publisher: str
    publication_year: int
    subject: str
    copies: List['BookCopy'] = field(default_factory=list)
    
    def add_copy(self, copy: 'BookCopy'):
        self.copies.append(copy)
    
    def get_available_copy(self) -> Optional['BookCopy']:
        """Get first available copy"""
        for copy in self.copies:
            if copy.status == BookStatus.AVAILABLE:
                return copy
        return None
    
    def available_count(self) -> int:
        return sum(1 for c in self.copies if c.status == BookStatus.AVAILABLE)
    
    def total_count(self) -> int:
        return len(self.copies)

class BookCopy:
    def __init__(self, book: Book, rack: RackLocation):
        self.barcode = str(uuid.uuid4())[:8].upper()
        self.book = book
        self.rack = rack
        self.status = BookStatus.AVAILABLE
        self.borrowed_by: Optional[str] = None  # Member ID
        self.due_date: Optional[datetime] = None
    
    def checkout(self, member_id: str) -> bool:
        if self.status != BookStatus.AVAILABLE:
            return False
        
        self.status = BookStatus.BORROWED
        self.borrowed_by = member_id
        self.due_date = datetime.now() + timedelta(days=LOAN_PERIOD_DAYS)
        return True
    
    def checkin(self) -> Optional[float]:
        """Return book and calculate fine if overdue"""
        if self.status != BookStatus.BORROWED:
            return None
        
        fine = 0.0
        if datetime.now() > self.due_date:
            overdue_days = (datetime.now() - self.due_date).days
            fine = overdue_days * FINE_PER_DAY
        
        self.status = BookStatus.AVAILABLE
        self.borrowed_by = None
        self.due_date = None
        
        return fine

Member Class

class Member:
    def __init__(self, name: str, email: str, phone: str):
        self.id = str(uuid.uuid4())[:8].upper()
        self.name = name
        self.email = email
        self.phone = phone
        self.status = MemberStatus.ACTIVE
        self.borrowed_books: List[BookCopy] = []
        self.reservations: List['Reservation'] = []
        self.total_fines = 0.0
        self.join_date = datetime.now()
    
    def can_borrow(self) -> bool:
        """Check if member can borrow more books"""
        if self.status != MemberStatus.ACTIVE:
            return False
        if len(self.borrowed_books) >= MAX_BOOKS_PER_MEMBER:
            return False
        if self.total_fines > 10.0:  # Outstanding fines limit
            return False
        return True
    
    def borrow_book(self, book_copy: BookCopy) -> Optional['Lending']:
        """Borrow a book"""
        if not self.can_borrow():
            raise Exception("Cannot borrow: limit reached or account suspended")
        
        if not book_copy.checkout(self.id):
            return None
        
        self.borrowed_books.append(book_copy)
        
        return Lending(
            id=str(uuid.uuid4()),
            member_id=self.id,
            book_copy=book_copy,
            borrow_date=datetime.now(),
            due_date=book_copy.due_date
        )
    
    def return_book(self, book_copy: BookCopy) -> float:
        """Return a book and get fine amount"""
        if book_copy not in self.borrowed_books:
            raise Exception("Book not borrowed by this member")
        
        fine = book_copy.checkin()
        self.borrowed_books.remove(book_copy)
        
        if fine > 0:
            self.total_fines += fine
        
        return fine
    
    def pay_fine(self, amount: float) -> bool:
        """Pay outstanding fines"""
        if amount <= 0 or amount > self.total_fines:
            return False
        
        self.total_fines -= amount
        
        # Reactivate if suspended and fines cleared
        if self.status == MemberStatus.SUSPENDED and self.total_fines == 0:
            self.status = MemberStatus.ACTIVE
        
        return True

Lending and Reservation

@dataclass
class Lending:
    id: str
    member_id: str
    book_copy: BookCopy
    borrow_date: datetime
    due_date: datetime
    return_date: Optional[datetime] = None
    fine_amount: float = 0.0
    
    def is_overdue(self) -> bool:
        if self.return_date:
            return False
        return datetime.now() > self.due_date
    
    def days_overdue(self) -> int:
        if not self.is_overdue():
            return 0
        return (datetime.now() - self.due_date).days

@dataclass
class Reservation:
    id: str
    member_id: str
    book: Book
    reservation_date: datetime
    status: ReservationStatus = ReservationStatus.PENDING
    expiry_date: datetime = None
    
    def __post_init__(self):
        if self.expiry_date is None:
            self.expiry_date = self.reservation_date + timedelta(days=MAX_RESERVATION_DAYS)
    
    def is_expired(self) -> bool:
        return datetime.now() > self.expiry_date
    
    def fulfill(self):
        self.status = ReservationStatus.FULFILLED
    
    def cancel(self):
        self.status = ReservationStatus.CANCELLED

Library System

import threading
from abc import ABC, abstractmethod

class SearchStrategy(ABC):
    @abstractmethod
    def search(self, books: Dict[str, Book], query: str) -> List[Book]:
        pass

class TitleSearchStrategy(SearchStrategy):
    def search(self, books: Dict[str, Book], query: str) -> List[Book]:
        return [b for b in books.values() if query.lower() in b.title.lower()]

class AuthorSearchStrategy(SearchStrategy):
    def search(self, books: Dict[str, Book], query: str) -> List[Book]:
        return [b for b in books.values() if query.lower() in b.author.lower()]

class ISBNSearchStrategy(SearchStrategy):
    def search(self, books: Dict[str, Book], query: str) -> List[Book]:
        book = books.get(query)
        return [book] if book else []

class Library:
    def __init__(self, name: str):
        self.name = name
        self.books: Dict[str, Book] = {}  # ISBN -> Book
        self.members: Dict[str, Member] = {}  # Member ID -> Member
        self.lendings: Dict[str, Lending] = {}  # Lending ID -> Lending
        self.reservations: List[Reservation] = []
        self._lock = threading.Lock()
    
    # Book Management
    def add_book(self, book: Book) -> bool:
        with self._lock:
            if book.isbn not in self.books:
                self.books[book.isbn] = book
                return True
            return False
    
    def add_book_copy(self, isbn: str, rack: RackLocation) -> Optional[BookCopy]:
        with self._lock:
            book = self.books.get(isbn)
            if book is None:
                return None
            
            copy = BookCopy(book, rack)
            book.add_copy(copy)
            return copy
    
    def search(self, query: str, strategy: SearchStrategy) -> List[Book]:
        return strategy.search(self.books, query)
    
    # Member Management
    def register_member(self, name: str, email: str, phone: str) -> Member:
        member = Member(name, email, phone)
        self.members[member.id] = member
        return member
    
    # Lending Operations
    def checkout_book(self, member_id: str, isbn: str) -> Optional[Lending]:
        with self._lock:
            member = self.members.get(member_id)
            book = self.books.get(isbn)
            
            if member is None or book is None:
                return None
            
            # Check for reservation by this member
            self._fulfill_reservation_if_exists(member, book)
            
            # Get available copy
            copy = book.get_available_copy()
            if copy is None:
                return None
            
            lending = member.borrow_book(copy)
            if lending:
                self.lendings[lending.id] = lending
            
            return lending
    
    def return_book(self, member_id: str, barcode: str) -> float:
        with self._lock:
            member = self.members.get(member_id)
            if member is None:
                raise Exception("Member not found")
            
            # Find the book copy
            book_copy = None
            for copy in member.borrowed_books:
                if copy.barcode == barcode:
                    book_copy = copy
                    break
            
            if book_copy is None:
                raise Exception("Book not found in member's borrowed list")
            
            fine = member.return_book(book_copy)
            
            # Check for pending reservations
            self._notify_next_reservation(book_copy.book)
            
            return fine
    
    # Reservation Operations
    def reserve_book(self, member_id: str, isbn: str) -> Optional[Reservation]:
        with self._lock:
            member = self.members.get(member_id)
            book = self.books.get(isbn)
            
            if member is None or book is None:
                return None
            
            # Check if book is available (no need to reserve)
            if book.get_available_copy() is not None:
                return None  # Can borrow directly
            
            # Check if already reserved by this member
            for res in self.reservations:
                if res.member_id == member_id and res.book.isbn == isbn:
                    if res.status == ReservationStatus.PENDING:
                        return None  # Already reserved
            
            reservation = Reservation(
                id=str(uuid.uuid4()),
                member_id=member_id,
                book=book,
                reservation_date=datetime.now()
            )
            
            self.reservations.append(reservation)
            member.reservations.append(reservation)
            
            return reservation
    
    def _fulfill_reservation_if_exists(self, member: Member, book: Book):
        for res in member.reservations:
            if res.book.isbn == book.isbn and res.status == ReservationStatus.PENDING:
                res.fulfill()
                break
    
    def _notify_next_reservation(self, book: Book):
        """Notify next person in reservation queue"""
        for res in self.reservations:
            if res.book.isbn == book.isbn and res.status == ReservationStatus.PENDING:
                if not res.is_expired():
                    self._send_notification(res.member_id, book)
                    break
                else:
                    res.status = ReservationStatus.EXPIRED
    
    def _send_notification(self, member_id: str, book: Book):
        member = self.members.get(member_id)
        if member:
            print(f"Notification sent to {member.email}: "
                  f"'{book.title}' is now available for pickup")
    
    # Reports
    def get_overdue_books(self) -> List[Lending]:
        return [l for l in self.lendings.values() if l.is_overdue()]
    
    def get_member_history(self, member_id: str) -> List[Lending]:
        return [l for l in self.lendings.values() if l.member_id == member_id]

Step 5: Usage Example

# Create library
library = Library("City Central Library")

# Add books
book1 = Book(
    isbn="978-0-13-468599-1",
    title="Clean Code",
    author="Robert C. Martin",
    publisher="Prentice Hall",
    publication_year=2008,
    subject="Software Engineering"
)
library.add_book(book1)

# Add multiple copies
rack = RackLocation(floor=2, section="CS", shelf=3)
library.add_book_copy("978-0-13-468599-1", rack)
library.add_book_copy("978-0-13-468599-1", rack)

# Register member
member = library.register_member(
    name="John Doe",
    email="[email protected]",
    phone="555-1234"
)

# Search for book
results = library.search("Clean Code", TitleSearchStrategy())
print(f"Found {len(results)} books")

# Borrow book
lending = library.checkout_book(member.id, "978-0-13-468599-1")
print(f"Borrowed: {lending.book_copy.book.title}, Due: {lending.due_date}")

# Return book (simulating after due date for fine)
import time
time.sleep(1)
fine = library.return_book(member.id, lending.book_copy.barcode)
print(f"Fine: ${fine:.2f}")

Key Design Decisions

A book (metadata) can have multiple physical copies. Each copy has its own barcode, status, and location. This allows tracking individual copies while sharing common book information.
When a book is returned, the person who reserved first should be notified first. A queue (list with FIFO behavior) naturally handles this fairness.
Interview Extension: Be ready to discuss handling book categories, late fees with grace periods, librarian vs member permissions, or integration with an online catalog.