15. Library Management System Intermediate

A. Realistic Interview Prompt

"Design a library management system. Members can search the catalog, check out and return items, place holds on items that are currently unavailable, and are charged a fine for overdue returns."

B. Difficulty

Intermediate. Every individual operation is simple; the risk is entirely in ending up with one Library god-class handling catalog search, checkout, holds, and fines all inline, instead of a clean split along genuinely different concerns.

C. What This Problem Tests

D. Requirements

Functional

  1. Search the catalog by title, author, or category
  2. Check out and return an item, tracking due dates
  3. Place a hold on an item with zero available copies; notify the next member in line when a copy is returned
  4. Calculate an overdue fine based on member type and item category
  5. Support multiple physical copies of the same title

Non-Functional

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1Does the fine formula vary by member type (student/faculty/regular) and/or item category?Determines whether fine calculation needs to be a Strategy or can be one constant rateDesign of FineStrategy
2How many physical copies can one title have, and are they distinguishable (different condition/location)?Confirms the Book/BookItem split is necessary rather than one merged entityPresence of BookItem as distinct from Book
3Are holds served strictly FIFO, or can priority (e.g. faculty) jump the queue?Affects the hold-queue data structure and notification orderDesign of HoldQueue
4Should overdue items eventually be treated as lost, with a replacement charge?Adds a lifecycle state beyond simple checked-out/returnedScope of BookItem status enum

F. Assumptions

G. Actors

ActorResponsibility
MemberSearches, checks out, returns, places holds
LibrarianManages catalog entries, resolves lost/damaged items
Library (facade)Top-level API coordinating catalog, checkout, and holds

H. Core Domain Entities

ClassResponsibilityKey AttributesKey MethodsDoes NOT Handle
Book (catalog metadata)Title-level information shared by every physical copytitle, author, ISBN, category—Anything about a specific copy's status or location
BookItem (physical copy)One lendable copy and its own lifecycle statusbarcode, status, dueDatecheckout(), returnItem()Title-level metadata — delegates to its Book reference
MemberA patron and their active loans/holdsid, membershipType—Fine calculation logic itself
LoanOne active checkout recordmember, bookItem, dueDateisOverdue()—
HoldOne member's place in a title's wait queuemember, book, timestamp——
HoldQueueFIFO queue of holds per title, and the Observer subject for availability—addHold(), notifyNext(bookItem)Deciding checkout eligibility itself
FineStrategy (interface)One fine-calculation rule—calculateFine(loan)—
CatalogSearch index over Book entries—search(query)Copy-level availability — a search result still needs a live BookItem lookup
Library (facade)Top-level orchestrationcatalog, holdQueuescheckout(), returnItem(), placeHold()—

I. Class Relationships

J. Class Diagram

1

*

*

Book

title

author

isbn

category

BookItem

barcode

status

dueDate

+checkout()

+returnItem()

Loan

dueDate

+isOverdue()

HoldQueue

+addHold()

+notifyNext(bookItem)

«interface»

FineStrategy

+calculateFine(loan)

Library

+checkout()

+returnItem()

+placeHold()

Hold

View Mermaid source
classDiagram
  class Book {
    title
    author
    isbn
    category
  }
  class BookItem {
    barcode
    status
    dueDate
    +checkout()
    +returnItem()
  }
  class Loan {
    dueDate
    +isOverdue()
  }
  class HoldQueue {
    +addHold()
    +notifyNext(bookItem)
  }
  class FineStrategy {
    <<interface>>
    +calculateFine(loan)
  }
  class Library {
    +checkout()
    +returnItem()
    +placeHold()
  }
  Book "1" --> "*" BookItem
  BookItem --> Loan
  HoldQueue --> "*" Hold
  Library --> HoldQueue
  Library --> FineStrategy

K. Responsibility Mapping

ClassResponsibilityDoes NOT Handle
BookTitle-level metadataCopy-level status
BookItemOne copy's lifecycleTitle metadata, fine amount
HoldQueueFIFO wait order and availability notification per titleCheckout eligibility decisions
FineStrategy implementationsOne fine formulaLoan record-keeping itself
LibraryTop-level orchestrationCatalog search internals, fine math

L. SOLID Analysis

PrincipleHow It's AppliedNaive Violation
SRPBook owns metadata; BookItem owns copy status; HoldQueue owns wait order; FineStrategy owns fine mathOne Library god-class with methods for search, checkout, holds, and fines all touching the same giant record type
OCPA new item category is a new Book subtype via Factory; a new fine rule is a new FineStrategyA fine-calculation if/else chain keyed on member type, edited every time a new membership tier is added
LSPEvery Book subtype (DVD, Magazine) honestly reports its own default loan duration without special-casing by callersCheckout logic doing if (item instanceof DVD) loanDays = 7 inline instead of asking the item itself
ISPFineStrategy stays a single-method interfaceA combined "LoanPolicy" interface mixing fine calculation with loan-duration and renewal-limit rules that don't always vary together
DIPLibrary depends on the FineStrategy abstraction, not a hardcoded rate tableLoan.calculateFine() embedding the rate table directly on the entity itself

M. Design Patterns

PatternProblem It SolvesWhere It AppearsWhen NOT to Use
ObserverAvailability notification shouldn't require the returning code path to know who's waitingHoldQueueSkip if holds are out of scope entirely
StrategyFine rates vary by member type and category and change more often than the rest of the systemFineStrategySkip if the round only ever needs one flat fine rate
Factory MethodConstructing the correct catalog item subtype from raw ingest dataCatalogItemFactorySkip if the catalog only ever holds one item type (books only, no DVDs/magazines)

Explicitly avoided: merging Book and BookItem into one entity — see I for why this is the most common and most damaging shortcut in this specific problem.

N. What Is Likely to Change?

BehaviorCurrent DesignAnticipated ChangeProtecting Abstraction
Fine ratesFixed per member/category combinationPromotional waivers, grace periods, tiered escalationFineStrategy interface
Item categoriesBooksDVDs, magazines, digital loans (e-books with auto-return)Book subtype hierarchy via Factory
Hold priorityStrict FIFOPriority queueing for certain member typesHoldQueue's internal ordering, same public notify contract

O. Design Evolution

What stays stable: Library's public checkout/return/hold API. New categories and fine rules are additive.

P. API / Interface Design

interface FineStrategy {
  Money calculateFine(Loan loan);
}

class Library {
  List<Book> search(String query);
  Loan checkout(Member member, String barcode);
  void returnItem(String barcode);
  void placeHold(Member member, Book book);
}

Ownership/validation notes: checkout() locates a specific BookItem with status AVAILABLE for the requested title, atomically transitions it to CHECKED_OUT, and creates the Loan — it never operates on the Book title level directly, since "available" is a copy-level fact.

Q. Pseudocode — Critical Logic

Library.checkout(member, barcode):
  lock(bookItemFor(barcode))
  try:
    item = bookItemFor(barcode)
    if item.status != AVAILABLE: reject
    item.status = CHECKED_OUT
    loan = new Loan(member, item, dueDate = now + loanPeriod(item.book.category))
    return loan
  finally:
    unlock(item)

BookItem.returnItem():
  this.status = AVAILABLE
  holdQueue = holdQueueFor(this.book)
  if holdQueue.hasWaiting():
    nextHold = holdQueue.popNext()
    this.status = ON_HOLD          // reserved for the notified member, not generally available
    notify(nextHold.member, this)

R. End-to-End Use Case

placeHold(memberB, "Clean Code") — no copies available, joins HoldQueue
  │
  ▼
memberA.returnItem(barcode)
  │  BookItem.status → AVAILABLE
  │  HoldQueue for "Clean Code" has memberB waiting → status → ON_HOLD, memberB notified
  ▼
memberB.checkout(barcode) within the hold window → status → CHECKED_OUT, Loan created

S. Data Structures

StructureUsed ForWhyAlternative & Trade-off
FIFO queue per title in HoldQueueHold orderingMatches the fairness expectation of "first to ask, first served"A priority queue if member-type-based priority holds are required (see N)
Inverted index (term → book IDs) in CatalogSearch by title/author/categorySub-linear lookup instead of scanning every book on every searchA linear scan is fine for a small catalog but doesn't hold up at real library scale

T. Complexity Analysis

OperationTimeSpace
checkout() / returnItem()O(1) given the copy's barcodeO(1)
search() (indexed)O(matching terms)O(index size)
Hold enqueue/notifyO(1) amortized (queue push/pop)O(waiting members)

U. Testing Strategy

V. Interviewer Follow-Ups

Follow-UpTestingDesign ChangeClasses ChangedPattern/Trade-off
"Add DVDs and magazines with different loan periods."Whether Book/BookItem was ever mergedNew Book subtypes with their own default loan durationNew subclasses + FactoryValidates the Book/BookItem split from I
"Faculty holds should jump ahead of student holds."Whether HoldQueue ordering was hardcoded FIFO logic scattered elsewhereSwap the queue's internal ordering to priority-basedHoldQueue onlyValidates that ordering was encapsulated, not leaked
"What if a book is reported lost?"Lifecycle completenessNew LOST status, replacement-cost charge via a dedicated strategyBookItem, a new fine/charge typeTests whether the status enum anticipated more than just available/checked-out

W. Concurrency

Shared mutable state: each BookItem's status field, particularly for the last remaining available copy of a popular title.

X. Persistence

Catalog entries, loans, holds, and fines are all durable records by nature — a library system loses all value if a checkout isn't remembered across a restart. Each is a natural repository-backed entity; none of this changes the in-memory class design, only where Library's operations ultimately read/write.

Y. Scaling Boundary

LayerWhat Belongs Here
LLDBook/BookItem model, hold queue, fine strategy, per-copy locking
HLDMulti-branch catalog federation, inter-library loan networks, digital-loan DRM
Explicitly out of scopeCross-branch/inter-library systems unless the round pivots there

Z. Failure Scenarios

AA. Common Bad Designs

AB. Design Trade-Offs

DecisionOption AOption BRecommendation
Hold expiry handlingAutomatic revert to AVAILABLE after a fixed windowManual librarian interventionAutomatic — a fixed, published hold-pickup window is standard real-library practice and avoids indefinitely locking a copy for a no-show
Catalog searchInverted indexLinear scanInverted index once catalog size is realistic; linear scan is a reasonable V1 for a small demo

AC. Interviewer Evaluation

Strong signal: splitting Book and BookItem unprompted; isolating fine calculation behind a Strategy before being asked for a second member type; catching the last-copy checkout race without being walked into it.

Red flag: one merged Book/copy entity; a god-object Library class; hardcoded fine constants; no expiry on holds.

AD. Interview Simulation

Interviewer: Design a library management system.
Candidate: I'd split a title's catalog metadata — Book — from a specific physical copy — BookItem — since a library can hold multiple copies of the same title, each with its own availability status.
Interviewer: What happens when a member wants a book that's fully checked out?
Candidate: They place a hold, which joins a FIFO queue for that title. When any copy is returned, that copy's return path notifies the queue, which pops the next waiting member and reserves the copy for them — an Observer relationship, so the return code doesn't need to know who, if anyone, is waiting.
Interviewer: How do fines work if students and faculty have different rates?
Candidate: I'd isolate fine calculation behind a FineStrategy keyed by member type and item category, rather than hardcoding rates on the loan itself — new membership tiers become a configuration change, not a code change.
Interviewer: Two members try to check out the last copy of a popular title at the same instant.
Candidate: Same fix as everywhere else this comes up — lock that specific copy, re-check its status is still available, then commit the checkout. Whoever's lock lands first gets it; the other is rejected cleanly, not left in an inconsistent state.

AE. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
GeeksforGeeks — System Design for Library ManagementRequirements-and-class-diagram walkthrough covering members, books, and transactionsClear requirements decomposition before jumping to classesPublic treatment doesn't consistently distinguish title-level Book from copy-level BookItem, and fine logic is presented as a fixed formula rather than a swappable strategyThe requirements-first framing is useful; this document's Book/BookItem split and Strategy-based fines go further
ashishps1/awesome-low-level-design (GitHub)A curated open-source repository of worked LLD problems, including library-management-style catalog and lending systemsReal, runnable code examples across many LLD problems in one place, useful for cross-checking class shapesBreadth-over-depth — individual problems get a working solution but not the same depth of concurrency/trade-off analysis as a dedicated write-upGood for spot-checking class-level structure against an independent, community-vetted source

AF. Interview Cheat Sheet

ProblemLibrary Management System
DifficultyIntermediate
Core requirementsCatalog search, checkout/return, holds with notification, member/category-aware fines
Key clarificationsFine formula variation? Multiple distinguishable copies? Hold priority rules? Lost-item handling?
Core entitiesBook, BookItem, Member, Loan, Hold, HoldQueue, FineStrategy, Catalog, Library
Key relationshipsBook/BookItem split is central; Observer for holds; Strategy for fines; Factory for item categories
Key patternsObserver, Strategy, Factory Method
Most important decisionSeparating title metadata (Book) from per-copy lifecycle state (BookItem)
Likely-changing behaviorFine rates, item categories, hold priority rules
Concurrency concernCheck-then-act race on checking out the last available copy of a title
Top follow-upsNew item categories, priority holds, lost-item lifecycle
Common mistakesMerged Book/BookItem entity; god-object Library class; hardcoded fine constants; no hold expiry
What NOT to over-engineerDon't design multi-branch/inter-library federation unless asked

AG. Full Implementation

A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.

// Library Management System - full LLD implementation (single-file demo).
// No "Concurrency" badge -> single-threaded; the "two members racing the last copy"
// requirement is satisfied by checkout() being one atomic status-check-and-flip step
// against a single BookItem, not by explicit locking primitives.
//
// Design notes:
// - Book (title metadata) vs. BookItem (physical copy) is the central modeling
//   decision: metadata is shared/immutable across copies, status is per-copy and
//   constantly changing.
// - Observer for holds: BookItem.returnItem() notifies the title's HoldQueue, which
//   pops the next Hold in FIFO order and hands the copy to that member - decoupling
//   "a copy became available" from "who reacts to that".
// - Strategy for fines: FineStrategy.calculateFine(loan) isolates rate changes from
//   Loan/Library's record-keeping code.
import java.util.*;

public class p15 {

    enum ItemStatus { AVAILABLE, CHECKED_OUT, ON_HOLD, LOST }
    enum Category { FICTION, NON_FICTION, REFERENCE }
    enum MembershipType { REGULAR, PREMIUM }

    // ---------- Book (title-level metadata) ----------
    static class Book {
        final String isbn, title, author;
        final Category category;
        Book(String isbn, String title, String author, Category category) {
            this.isbn = isbn; this.title = title; this.author = author; this.category = category;
        }
    }

    // ---------- BookItem (one physical, lendable copy) ----------
    static class BookItem {
        final String barcode;
        final Book book;
        ItemStatus status = ItemStatus.AVAILABLE;
        Date dueDate;
        final Library library; // back-reference used to reach the right HoldQueue on return

        BookItem(String barcode, Book book, Library library) {
            this.barcode = barcode; this.book = book; this.library = library;
        }

        // Marks the copy checked out; Library owns creating/recording the Loan itself.
        void checkout(Date dueDate) {
            this.status = ItemStatus.CHECKED_OUT;
            this.dueDate = dueDate;
        }

        // Flips back to available, then defers to the title's HoldQueue (Observer) to
        // decide whether this copy should instead go straight to the next waiting member.
        void returnItem() {
            this.status = ItemStatus.AVAILABLE;
            this.dueDate = null;
            HoldQueue queue = library.holdQueueFor(book);
            if (queue != null && queue.hasWaiting()) {
                Hold nextHold = queue.popNext();
                this.status = ItemStatus.ON_HOLD; // reserved, not generally available
                queue.notifyNext(this, nextHold);
            }
        }
    }

    // ---------- Member ----------
    static class Member {
        final String id;
        final MembershipType membershipType;
        final List<Loan> activeLoans = new ArrayList<>();
        final List<Hold> activeHolds = new ArrayList<>();
        Member(String id, MembershipType membershipType) { this.id = id; this.membershipType = membershipType; }
    }

    // ---------- Loan (one active checkout record) ----------
    static class Loan {
        final Member member;
        final BookItem bookItem;
        final Date checkoutDate;
        final Date dueDate;
        boolean returned = false;

        Loan(Member member, BookItem bookItem, Date checkoutDate, Date dueDate) {
            this.member = member; this.bookItem = bookItem; this.checkoutDate = checkoutDate; this.dueDate = dueDate;
        }

        boolean isOverdue(Date asOf) { return !returned && asOf.after(dueDate); }
    }

    // ---------- Hold (one member's place in a title's wait queue) ----------
    static class Hold {
        final Member member;
        final Book book;
        final long timestamp;
        Hold(Member member, Book book, long timestamp) { this.member = member; this.book = book; this.timestamp = timestamp; }
    }

    // ---------- HoldQueue (FIFO wait list + Observer subject for availability) ----------
    static class HoldQueue {
        private final Deque<Hold> queue = new ArrayDeque<>();

        void addHold(Hold hold) { queue.addLast(hold); }
        boolean hasWaiting() { return !queue.isEmpty(); }
        Hold popNext() { return queue.pollFirst(); }
        int waitingCount() { return queue.size(); }

        // The "reaction" side of the Observer relationship: BookItem triggers this,
        // HoldQueue decides who gets notified. A real system would email/page the
        // member here; we just print, and record it as satisfied.
        void notifyNext(BookItem item, Hold hold) {
            hold.member.activeHolds.remove(hold);
            System.out.println("  Notifying " + hold.member.id + ": your hold on \"" + item.book.title + "\" is ready (barcode " + item.barcode + ")");
        }
    }

    // ---------- FineStrategy (interface, Strategy) ----------
    interface FineStrategy {
        double calculateFine(Loan loan, Date asOf);
    }

    // Rate depends on both member type and item category - isolated here so a rate
    // change never touches Loan or Library.
    static class StandardFineStrategy implements FineStrategy {
        public double calculateFine(Loan loan, Date asOf) {
            if (!loan.isOverdue(asOf)) return 0.0;
            long overdueDays = (asOf.getTime() - loan.dueDate.getTime()) / (1000L * 60 * 60 * 24) + 1;
            double perDay = loan.bookItem.book.category == Category.REFERENCE ? 1.0 : 0.25;
            if (loan.member.membershipType == MembershipType.PREMIUM) perDay *= 0.5; // premium discount
            return overdueDays * perDay;
        }
    }

    // ---------- Catalog (search index over Book entries) ----------
    static class Catalog {
        private final List<Book> books = new ArrayList<>();
        void addBook(Book book) { books.add(book); }

        List<Book> search(String query) {
            String q = query.toLowerCase();
            List<Book> results = new ArrayList<>();
            for (Book b : books) {
                if (b.title.toLowerCase().contains(q) || b.author.toLowerCase().contains(q)
                        || b.category.name().toLowerCase().contains(q)) {
                    results.add(b);
                }
            }
            return results;
        }
    }

    // ---------- Library (facade) ----------
    static class Library {
        final Catalog catalog = new Catalog();
        final Map<String, BookItem> itemsByBarcode = new HashMap<>();
        final Map<String, Loan> activeLoanByBarcode = new HashMap<>();
        final Map<String, HoldQueue> holdQueuesByIsbn = new HashMap<>();
        final FineStrategy fineStrategy;
        private static final long LOAN_PERIOD_MILLIS = 14L * 24 * 60 * 60 * 1000; // 14 days, flat for demo

        Library(FineStrategy fineStrategy) { this.fineStrategy = fineStrategy; }

        void addBookItem(BookItem item) { itemsByBarcode.put(item.barcode, item); }

        HoldQueue holdQueueFor(Book book) { return holdQueuesByIsbn.get(book.isbn); }

        List<Book> search(String query) { return catalog.search(query); }

        // Single atomic status-check-and-flip against one BookItem - this is exactly
        // what stops two members both succeeding against what they each believe is
        // "the last copy".
        Loan checkout(Member member, String barcode) {
            BookItem item = itemsByBarcode.get(barcode);
            if (item == null) throw new IllegalArgumentException("No such item: " + barcode);
            if (item.status != ItemStatus.AVAILABLE) return null; // reject: not free to check out

            Date due = new Date(System.currentTimeMillis() + LOAN_PERIOD_MILLIS);
            item.checkout(due);
            Loan loan = new Loan(member, item, new Date(), due);
            member.activeLoans.add(loan);
            activeLoanByBarcode.put(barcode, loan);
            return loan;
        }

        void returnItem(String barcode) {
            BookItem item = itemsByBarcode.get(barcode);
            if (item == null) throw new IllegalArgumentException("No such item: " + barcode);
            Loan loan = activeLoanByBarcode.remove(barcode);
            if (loan != null) {
                loan.returned = true;
                loan.member.activeLoans.remove(loan);
            }
            item.returnItem(); // triggers the HoldQueue Observer hook internally
        }

        double calculateFine(Loan loan) { return fineStrategy.calculateFine(loan, new Date()); }

        // Places a hold only when no copy is free; otherwise the member should just checkout.
        void placeHold(Member member, Book book) {
            HoldQueue queue = holdQueuesByIsbn.computeIfAbsent(book.isbn, k -> new HoldQueue());
            Hold hold = new Hold(member, book, System.currentTimeMillis());
            queue.addHold(hold);
            member.activeHolds.add(hold);
        }
    }

    // ---------- Demo ----------
    public static void main(String[] args) throws InterruptedException {
        Library library = new Library(new StandardFineStrategy());

        Book book = new Book("978-1", "The Pragmatic Programmer", "Hunt & Thomas", Category.NON_FICTION);
        library.catalog.addBook(book);
        BookItem copy1 = new BookItem("BC-1", book, library);
        BookItem copy2 = new BookItem("BC-2", book, library);
        library.addBookItem(copy1);
        library.addBookItem(copy2);

        System.out.println("Search 'pragmatic': " + library.search("pragmatic").size() + " result(s)");

        Member alice = new Member("alice", MembershipType.REGULAR);
        Member bob = new Member("bob", MembershipType.PREMIUM);
        Member carol = new Member("carol", MembershipType.REGULAR);

        Loan loan1 = library.checkout(alice, "BC-1");
        Loan loan2 = library.checkout(bob, "BC-2");
        System.out.println("Alice checked out BC-1: " + (loan1 != null));
        System.out.println("Bob checked out BC-2: " + (loan2 != null));

        // Both copies are gone now - Carol must queue for a hold instead of a 3rd checkout.
        Loan failedLoan = library.checkout(carol, "BC-1");
        System.out.println("Carol tries BC-1 while checked out: " + (failedLoan == null ? "rejected as expected" : "unexpectedly succeeded"));
        library.placeHold(carol, book);
        System.out.println("Carol placed a hold; queue size: " + library.holdQueueFor(book).waitingCount());

        // Alice returns her copy -> Observer hook should notify Carol and reserve BC-1 for her.
        library.returnItem("BC-1");
        System.out.println("BC-1 status after return: " + copy1.status);

        // Simulate an overdue fine: backdate the due date on Bob's loan and price it.
        loan2.dueDate.setTime(System.currentTimeMillis() - (3L * 24 * 60 * 60 * 1000));
        double fine = library.calculateFine(loan2);
        System.out.println("Bob's fine for 3 overdue days (premium discount applied): $" + fine);
    }
}