LLD · Use Cases to Code

Low Level Design: Library Management System

A full walkthrough from actors and use cases to a working Java model: physical copies vs titles, loan limits, fines, a fair reservation queue with hold expiry, and notifications that don't leak into business logic.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 18 min read
Modern library with glowing barcodes on books, a queue of figures at the desk and notification envelopes flying out
TL;DR
  • Start from actors and use cases (member, librarian, system scheduler). They tell you which classes and methods must exist.
  • Separate Book (title, ISBN) from BookCopy (barcode, status). Loans and holds attach to copies; search and reservations attach to titles.
  • Reservations are a FIFO queue per title. A returned copy goes ON_HOLD for the first in line with an expiry; expired holds pass to the next member.
  • Fines are a FinePolicy strategy (per-day rate by member type, capped). Unpaid fines block new loans; reservations block renewals.
  • Notifications use Observer, so email, SMS or WhatsApp channels plug in without touching loan logic. Full Java code with a dated scenario below.

The library management system is the LLD question that most rewards structured thinking. There is no clever algorithm and no tricky concurrency puzzle at its centre. Instead there are a lot of business rules (limits, due dates, renewals, holds, fines), and the interviewer is watching whether you can organise them into a model where each rule has exactly one home.

That makes it a great problem to practise the classic LLD workflow: actors → use cases → rules → entities → classes → code. If you are new to that workflow, the LLD interview preparation guide explains it; this post applies it end to end.

Actors and Use Cases

Listing actors first stops you from designing a system for nobody. Each actor's goals become use cases, and each use case becomes a method on the service.

ActorUse cases
Member (student, faculty)Search catalog, check out, return, renew, reserve a title, cancel reservation, view loans and fines, pay fine
LibrarianAdd titles and copies, register members, issue and accept returns at the desk, mark copies lost or damaged, waive fines
System (daily scheduler)Expire uncollected holds, send due-soon reminders, send overdue notices
Notification channelsDeliver messages (email, SMS, in-app); passive receivers

Notice that the scheduler is an actor. Many designs forget that holds expire and reminders go out without any human pressing a button, and end up with no place to put that logic.

Business Rules to Confirm

Propose concrete defaults; interviewers prefer that to open-ended questions. These are the ones this design implements:

RuleStudentFaculty
Max books on loan510
Loan period14 days30 days
Fine per late day (example)₹5₹2
Fine cap per loan₹500
RenewalsMax 2; not if overdue; not if anyone has reserved the title
Borrowing with unpaid finesBlocked until paid
Hold pickup window3 days, then passes to the next in queue
Reserve when a copy is on the shelfNot allowed; borrow it instead

Rates and limits differ between institutions; the point is that they live in data (an enum or config), not scattered through if statements.

The Key Modelling Decision: Book vs BookCopy

A library may own five copies of Designing Data-Intensive Applications. "The book" means two different things:

  • Book: the title. ISBN, title, author, subject. Shared by all copies. This is what members search for and reserve ("I want any copy").
  • BookCopy: one physical object with a barcode. It has a status (AVAILABLE, LOANED, ON_HOLD, LOST), a shelf location, a condition. This is what is checked out, returned and held.

Get this wrong and everything downstream breaks: you cannot have two copies on loan at once, or a reservation waits for one specific copy while another identical copy sits on the shelf. Interviewers often ask "how many copies of this book are available?" specifically to see if your model can answer it.

💡
Loan is an entity, not a fieldResist putting borrowedBy and dueDate on BookCopy. A Loan has its own lifecycle (issued, renewed, returned, fined) and history. Making it a separate class keeps BookCopy simple and gives you a loan history for free.

BookCopy lifecycle

┌───────────┐ checkout ┌────────┐ return, queue empty ┌───────────┐ │ AVAILABLE │ ──────────► │ LOANED │ ────────────────────► │ AVAILABLE │ └───────────┘ └────────┘ └───────────┘ ▲ │ return, someone reserved │ hold expired, ▼ │ queue empty ┌─────────┐ reserver checks out ┌────────┐ └────────────────── │ ON_HOLD │ ─────────────────────► │ LOANED │ └─────────┘ └────────┘ hold expired, next in queue: stays ON_HOLD for that member LOANED / AVAILABLE ──(librarian marks)──► LOST

Library Management System Class Design

┌─────────────────────────────────────────────────────────────────┐ │ LibraryService (facade) │ │ checkout · returnCopy · renew · reserve · payFine · dailyJobs │ └───┬──────────────┬───────────────┬──────────────┬───────────────┘ │ │ │ │ notifies ▼ ▼ ▼ ▼ ┌─────────┐ ┌───────────┐ ┌─────────────┐ ┌────────────────────┐ │ Catalog │ │ Member │ │ FinePolicy │ │ «LibraryListener» │ │ search │ │ type,fines│ │ PerDay │ │ EmailNotifier │ │ index │ │ loans │ └─────────────┘ │ SmsNotifier ... │ └───┬─────┘ └─────┬─────┘ └────────────────────┘ │ 1..* │ 0..max ▼ ▼ ┌────────┐ 1..* ┌──────────┐ 0..1 ┌────────────────────────────┐ │ Book │◄─────│ BookCopy │◄─────│ Loan(issuedOn, dueOn, │ │ (ISBN) │ │ barcode, │ │ renewals) │ └────────┘ │ status │ └────────────────────────────┘ ▲ └──────────┘ └── reservations: ISBN → FIFO queue of member ids
ClassOwnsDoes not own
CatalogTitles, copies, search indexWho borrowed what
MemberType, unpaid fine, active loan barcodesFine rules (those are policy)
LoanDates, renewalsFine computation
FinePolicyHow much a late return costsWhether borrowing is allowed
LibraryServiceOrchestration, rule enforcement, reservation queuesHow messages are delivered

That "does not own" column is the single responsibility principle made concrete, and it is a good thing to say out loud. The SOLID principles guide has more examples of this style of reasoning.

Reservations and Holds: A Fair Queue

Reservation is the most interesting part of this problem, because it couples several rules:

  1. Reserve the title, not a copy. When no copy is available, the member joins a FIFO queue keyed by ISBN.
  2. On return, check the queue first. If someone is waiting, the copy does not go back to the shelf. It becomes ON_HOLD for the first member, with a pickup deadline, and that member is notified.
  3. Only the holder can check out a held copy. Anyone else is told it is on hold.
  4. Holds expire. A daily job finds holds past their deadline, notifies the member, and passes the copy to the next in queue (or back to AVAILABLE).
  5. Reservations block renewals. Otherwise a borrower could renew forever while others wait.

Why FIFO? It is what members perceive as fair, and it is easy to explain. Priority queues (faculty first) are a reasonable extension: replace the Deque with a PriorityQueue ordered by (priority, reservation time). The rest of the logic is unchanged.

Fine Calculation as a Strategy

The basic rule is lateDays × ratePerDay, capped. In Java, ChronoUnit.DAYS.between(dueOn, returnedOn) gives late days directly; clamp negatives to zero. Libraries change fine rules (grace periods, exam-season amnesty, weekends excluded, higher rates for reference books), so the rule lives behind a FinePolicy interface. The service calls finePolicy.fineFor(loan, today) and does not know the formula.

Dates come from an injected "today" rather than LocalDate.now() inside the logic. That one choice makes every date-based rule testable: the scenario below moves time forward by setting the date.

Notifications with the Observer Pattern

Members need messages when a hold is ready, when it expires, when a book is due tomorrow, when it is overdue, and when a fine is charged. The tempting implementation calls emailService.send(...) inside returnCopy. Then the institution asks for SMS, then WhatsApp, then an in-app inbox, and the core method grows a dependency on each.

Instead, LibraryService publishes domain events to a list of LibraryListeners. An EmailNotifier, an SmsNotifier and an analytics recorder each subscribe. The service has no idea how messages are delivered. That is Observer, and it is also the Dependency Inversion principle: high-level policy depends on an abstraction, not on SMTP. In a larger system the same events would go onto a message queue so delivery failures do not affect checkout; see Kafka vs RabbitMQ vs SQS for that next step.

Full Java Code for the Library Management System

Java 17+, single file, run with java LibraryDemo.java.

Java
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.*;

public class LibraryDemo {

    enum MemberType {
        STUDENT(5, 14, 5), FACULTY(10, 30, 2);

        final int maxLoans;
        final int loanDays;
        final long finePerDay;

        MemberType(int maxLoans, int loanDays, long finePerDay) {
            this.maxLoans = maxLoans;
            this.loanDays = loanDays;
            this.finePerDay = finePerDay;
        }
    }

    enum CopyStatus { AVAILABLE, LOANED, ON_HOLD, LOST }

    record Book(String isbn, String title, String author, String subject) { }

    static final class BookCopy {
        final String barcode;
        final Book book;
        CopyStatus status = CopyStatus.AVAILABLE;
        String heldFor;          // member id while ON_HOLD
        LocalDate holdUntil;

        BookCopy(String barcode, Book book) {
            this.barcode = barcode;
            this.book = book;
        }
    }

    static final class Member {
        final String id;
        final String name;
        final MemberType type;
        long fineDue;
        final Set<String> activeLoans = new HashSet<>();   // barcodes

        Member(String id, String name, MemberType type) {
            this.id = id;
            this.name = name;
            this.type = type;
        }
    }

    static final class Loan {
        final BookCopy copy;
        final Member member;
        final LocalDate issuedOn;
        LocalDate dueOn;
        int renewals;

        Loan(BookCopy copy, Member member, LocalDate issuedOn, LocalDate dueOn) {
            this.copy = copy;
            this.member = member;
            this.issuedOn = issuedOn;
            this.dueOn = dueOn;
        }
    }

    static final class LibraryException extends RuntimeException {
        LibraryException(String message) { super(message); }
    }

    // ===================== Fine policy (Strategy) =====================
    interface FinePolicy {
        long fineFor(Loan loan, LocalDate returnedOn);
    }

    static final class PerDayFinePolicy implements FinePolicy {
        private final long capPerLoan;
        PerDayFinePolicy(long capPerLoan) { this.capPerLoan = capPerLoan; }

        public long fineFor(Loan loan, LocalDate returnedOn) {
            long lateDays = Math.max(0, ChronoUnit.DAYS.between(loan.dueOn, returnedOn));
            return Math.min(capPerLoan, lateDays * loan.member.type.finePerDay);
        }
    }

    // ===================== Notifications (Observer) =====================
    interface LibraryListener {
        default void holdReady(Member m, BookCopy c) { }
        default void holdExpired(Member m, BookCopy c) { }
        default void dueSoon(Member m, Loan l) { }
        default void overdue(Member m, Loan l, long days) { }
        default void fineCharged(Member m, long amount) { }
    }

    static final class EmailNotifier implements LibraryListener {
        public void holdReady(Member m, BookCopy c) {
            send(m, "'" + c.book.title() + "' is ready for pickup until " + c.holdUntil);
        }
        public void holdExpired(Member m, BookCopy c) { send(m, "Your hold on '" + c.book.title() + "' expired"); }
        public void dueSoon(Member m, Loan l) { send(m, "'" + l.copy.book.title() + "' is due on " + l.dueOn); }
        public void overdue(Member m, Loan l, long days) {
            send(m, "'" + l.copy.book.title() + "' is " + days + " day(s) overdue");
        }
        public void fineCharged(Member m, long amount) { send(m, "Fine of Rs " + amount + " charged"); }

        private void send(Member m, String msg) { System.out.println("  [email to " + m.name + "] " + msg); }
    }

    // ===================== Catalog =====================
    static final class Catalog {
        private final Map<String, Book> books = new HashMap<>();
        private final Map<String, List<BookCopy>> copiesByIsbn = new HashMap<>();
        private final Map<String, BookCopy> copiesByBarcode = new LinkedHashMap<>();
        private final Map<String, Set<String>> index = new HashMap<>();   // token -> ISBNs

        void addBook(Book b) {
            books.put(b.isbn(), b);
            copiesByIsbn.putIfAbsent(b.isbn(), new ArrayList<>());
            for (String field : List.of(b.title(), b.author(), b.subject())) {
                for (String token : field.toLowerCase().split("\\W+")) {
                    if (!token.isBlank()) index.computeIfAbsent(token, k -> new HashSet<>()).add(b.isbn());
                }
            }
        }

        void addCopy(String isbn, String barcode) {
            Book b = books.get(isbn);
            if (b == null) throw new LibraryException("Unknown ISBN " + isbn);
            if (copiesByBarcode.containsKey(barcode)) throw new LibraryException("Duplicate barcode " + barcode);
            BookCopy c = new BookCopy(barcode, b);
            copiesByBarcode.put(barcode, c);
            copiesByIsbn.get(isbn).add(c);
        }

        /** Every query word must match the title, author or subject. */
        List<Book> search(String query) {
            Set<String> result = null;
            for (String token : query.toLowerCase().split("\\W+")) {
                if (token.isBlank()) continue;
                Set<String> hits = index.getOrDefault(token, Set.of());
                if (result == null) result = new HashSet<>(hits);
                else result.retainAll(hits);
            }
            if (result == null) return List.of();
            return result.stream().map(books::get).sorted(Comparator.comparing(Book::title)).toList();
        }

        BookCopy copy(String barcode) {
            BookCopy c = copiesByBarcode.get(barcode);
            if (c == null) throw new LibraryException("Unknown barcode " + barcode);
            return c;
        }

        List<BookCopy> copiesOf(String isbn) {
            List<BookCopy> list = copiesByIsbn.get(isbn);
            if (list == null) throw new LibraryException("Unknown ISBN " + isbn);
            return list;
        }

        Collection<BookCopy> allCopies() { return copiesByBarcode.values(); }
    }

    // ===================== Service (Facade) =====================
    static final class LibraryService {
        private static final int HOLD_DAYS = 3;
        private static final int MAX_RENEWALS = 2;

        private final Catalog catalog = new Catalog();
        private final Map<String, Member> members = new HashMap<>();
        private final Map<String, Loan> activeLoans = new HashMap<>();            // barcode -> loan
        private final Map<String, Deque<String>> reservations = new HashMap<>(); // ISBN -> member ids
        private final FinePolicy finePolicy;
        private final List<LibraryListener> listeners = new ArrayList<>();
        private LocalDate today;

        LibraryService(FinePolicy finePolicy, LocalDate today) {
            this.finePolicy = finePolicy;
            this.today = today;
        }

        Catalog catalog() { return catalog; }
        synchronized void setToday(LocalDate d) { today = d; }
        void subscribe(LibraryListener l) { listeners.add(l); }

        synchronized void addBook(Book b, String... barcodes) {
            catalog.addBook(b);
            for (String code : barcodes) catalog.addCopy(b.isbn(), code);
        }

        synchronized void registerMember(String id, String name, MemberType type) {
            if (members.putIfAbsent(id, new Member(id, name, type)) != null) throw new LibraryException("Duplicate member " + id);
        }

        synchronized Loan checkout(String memberId, String barcode) {
            Member m = member(memberId);
            BookCopy c = catalog.copy(barcode);
            if (m.fineDue > 0) throw new LibraryException(m.name + " has unpaid fines of Rs " + m.fineDue);
            if (m.activeLoans.size() >= m.type.maxLoans) {
                throw new LibraryException(m.name + " already has " + m.type.maxLoans + " books");
            }
            boolean heldForMe = c.status == CopyStatus.ON_HOLD && memberId.equals(c.heldFor);
            if (c.status == CopyStatus.ON_HOLD && !heldForMe) {
                throw new LibraryException("'" + c.book.title() + "' (" + barcode + ") is on hold for another member");
            }
            if (c.status != CopyStatus.AVAILABLE && !heldForMe) {
                throw new LibraryException("'" + c.book.title() + "' (" + barcode + ") is " + c.status);
            }
            c.status = CopyStatus.LOANED;
            c.heldFor = null;
            c.holdUntil = null;
            Loan loan = new Loan(c, m, today, today.plusDays(m.type.loanDays));
            activeLoans.put(barcode, loan);
            m.activeLoans.add(barcode);
            return loan;
        }

        synchronized long returnCopy(String barcode) {
            Loan loan = activeLoans.remove(barcode);
            if (loan == null) throw new LibraryException(barcode + " is not on loan");
            Member m = loan.member;
            m.activeLoans.remove(barcode);
            long fine = finePolicy.fineFor(loan, today);
            if (fine > 0) {
                m.fineDue += fine;
                listeners.forEach(l -> l.fineCharged(m, fine));
            }
            passToNextReserver(loan.copy);
            return fine;
        }

        synchronized void reserve(String memberId, String isbn) {
            Member m = member(memberId);
            List<BookCopy> copies = catalog.copiesOf(isbn);
            if (copies.stream().anyMatch(c -> c.status == CopyStatus.AVAILABLE)) {
                throw new LibraryException("A copy is on the shelf; borrow it instead");
            }
            if (copies.stream().anyMatch(c -> m.activeLoans.contains(c.barcode))) {
                throw new LibraryException(m.name + " already has this title");
            }
            Deque<String> queue = reservations.computeIfAbsent(isbn, k -> new ArrayDeque<>());
            if (queue.contains(memberId)) throw new LibraryException(m.name + " already reserved this title");
            queue.addLast(memberId);
        }

        synchronized Loan renew(String memberId, String barcode) {
            Loan loan = activeLoans.get(barcode);
            if (loan == null || !loan.member.id.equals(memberId)) throw new LibraryException("No such loan");
            if (!reservations.getOrDefault(loan.copy.book.isbn(), new ArrayDeque<>()).isEmpty()) {
                throw new LibraryException("Cannot renew: other members are waiting for this book");
            }
            if (today.isAfter(loan.dueOn)) throw new LibraryException("Cannot renew an overdue book");
            if (loan.renewals >= MAX_RENEWALS) throw new LibraryException("Renewal limit reached");
            loan.dueOn = loan.dueOn.plusDays(loan.member.type.loanDays);
            loan.renewals++;
            return loan;
        }

        synchronized void payFine(String memberId, long amount) {
            Member m = member(memberId);
            if (amount <= 0 || amount > m.fineDue) throw new LibraryException("Invalid amount");
            m.fineDue -= amount;
        }

        /** Run once a day by a scheduler. */
        synchronized void runDailyJobs() {
            for (BookCopy c : catalog.allCopies()) {
                if (c.status == CopyStatus.ON_HOLD && today.isAfter(c.holdUntil)) {
                    Member m = members.get(c.heldFor);
                    listeners.forEach(l -> l.holdExpired(m, c));
                    passToNextReserver(c);
                }
            }
            for (Loan loan : activeLoans.values()) {
                long daysLeft = ChronoUnit.DAYS.between(today, loan.dueOn);
                if (daysLeft == 1) listeners.forEach(l -> l.dueSoon(loan.member, loan));
                else if (daysLeft < 0) listeners.forEach(l -> l.overdue(loan.member, loan, -daysLeft));
            }
        }

        private void passToNextReserver(BookCopy c) {
            Deque<String> queue = reservations.getOrDefault(c.book.isbn(), new ArrayDeque<>());
            String next = queue.pollFirst();
            if (next == null) {
                c.status = CopyStatus.AVAILABLE;
                c.heldFor = null;
                c.holdUntil = null;
                return;
            }
            c.status = CopyStatus.ON_HOLD;
            c.heldFor = next;
            c.holdUntil = today.plusDays(HOLD_DAYS);
            Member m = members.get(next);
            listeners.forEach(l -> l.holdReady(m, c));
        }

        private Member member(String id) {
            Member m = members.get(id);
            if (m == null) throw new LibraryException("Unknown member " + id);
            return m;
        }
    }

    static void attempt(Runnable action) {
        try {
            action.run();
        } catch (LibraryException e) {
            System.out.println("   ! " + e.getMessage());
        }
    }

    static List<String> titles(List<Book> books) { return books.stream().map(Book::title).toList(); }

    public static void main(String[] args) {
        LibraryService lib = new LibraryService(new PerDayFinePolicy(500), LocalDate.of(2026, 10, 1));
        lib.subscribe(new EmailNotifier());
        lib.addBook(new Book("9781449373320", "Designing Data-Intensive Applications",
                             "Martin Kleppmann", "Distributed Systems"), "DDIA-1");
        lib.addBook(new Book("9780132350884", "Clean Code", "Robert C. Martin", "Software Craftsmanship"),
                    "CC-1", "CC-2");
        lib.registerMember("M1", "Riya", MemberType.STUDENT);
        lib.registerMember("M2", "Arjun", MemberType.STUDENT);
        lib.registerMember("M3", "Prof. Mehta", MemberType.FACULTY);

        System.out.println("Search 'data systems': " + titles(lib.catalog().search("data systems")));

        Loan riyaLoan = lib.checkout("M1", "DDIA-1");
        System.out.println("Riya borrowed DDIA, due " + riyaLoan.dueOn);
        lib.reserve("M2", "9781449373320");                       // Arjun joins the queue

        lib.setToday(LocalDate.of(2026, 10, 10));
        attempt(() -> lib.renew("M1", "DDIA-1"));                 // blocked by Arjun's reservation

        lib.setToday(LocalDate.of(2026, 10, 18));
        System.out.println("Riya returned DDIA 3 days late, fine Rs " + lib.returnCopy("DDIA-1"));
        attempt(() -> lib.checkout("M3", "DDIA-1"));              // on hold for Arjun
        attempt(() -> lib.checkout("M1", "CC-1"));                // Riya has an unpaid fine
        lib.payFine("M1", 15);
        System.out.println("Riya borrowed Clean Code, due " + lib.checkout("M1", "CC-1").dueOn);

        lib.setToday(LocalDate.of(2026, 10, 22));
        lib.runDailyJobs();                                       // Arjun never collected
        System.out.println("DDIA-1 is now " + lib.catalog().copy("DDIA-1").status);
    }
}

Scenario Walkthrough

Output
Search 'data systems': [Designing Data-Intensive Applications]
Riya borrowed DDIA, due 2026-10-15
   ! Cannot renew: other members are waiting for this book
  [email to Riya] Fine of Rs 15 charged
  [email to Arjun] 'Designing Data-Intensive Applications' is ready for pickup until 2026-10-21
Riya returned DDIA 3 days late, fine Rs 15
   ! 'Designing Data-Intensive Applications' (DDIA-1) is on hold for another member
   ! Riya has unpaid fines of Rs 15
Riya borrowed Clean Code, due 2026-11-01
  [email to Arjun] Your hold on 'Designing Data-Intensive Applications' expired
DDIA-1 is now AVAILABLE
  1. Search. "data" matches the title, "systems" matches the subject; both must match, so only DDIA is returned. The inverted index makes this O(tokens), not a scan of every book.
  2. 1 Oct: Riya borrows the only copy. Student loan period 14 days, so due 15 Oct. Arjun reserves the title and joins the queue.
  3. 10 Oct: renewal refused because the reservation queue for that ISBN is not empty.
  4. 18 Oct: late return. Three days late × ₹5 = ₹15. The fine email fires, then the copy goes ON_HOLD for Arjun until 21 Oct and he is notified. The emails print before the return line because returnCopy runs while the string is being built.
  5. Prof. Mehta cannot take the held copy, and Riya cannot borrow anything while she owes ₹15. After paying, she borrows Clean Code until 1 Nov.
  6. 22 Oct: hold expires. The daily job notifies Arjun, finds the queue empty, and returns the copy to AVAILABLE.

Persistence, Concurrency and Scale

Interviewers often follow up with "how would this look with a database?" A minimal relational schema maps directly from the classes:

TableKey columnsNotes
bookisbn (PK), title, author, subjectFull-text index or a search engine for discovery
book_copybarcode (PK), isbn (FK), status, held_for, hold_until, versionIndex on (isbn, status)
memberid (PK), name, type, fine_due
loanid (PK), barcode, member_id, issued_on, due_on, returned_on, renewalsPartial index on active loans; index on due_on for reminder jobs
reservationid (PK), isbn, member_id, created_at, statusQueue order = created_at; unique (isbn, member_id) for active rows
fine_txnid, member_id, amount, reason, created_atLedger instead of a single mutable number for audits

Concurrency: two desks scanning the same copy, or a member reserving while a librarian processes a return. In memory, the facade's synchronized methods serialise operations, which is fine for one branch. In a database, checkout becomes one transaction: UPDATE book_copy SET status='LOANED', version=version+1 WHERE barcode=? AND status='AVAILABLE' AND version=?, and if zero rows change, someone else won. The ACID and isolation levels guide explains why this conditional update is safe, and the DBMS interview questions cover locking in more depth.

Scale: a university library is small data; one relational database is plenty. Search can move to Elasticsearch when the catalogue includes full text or many branches; the reasoning is in Elasticsearch and inverted indexes. The daily jobs become scheduled tasks that page through holds and loans by date index.

Interview Tips for Library Management LLD

Follow-ups to expect

  • Multiple branches: add a Branch to BookCopy; reservations may specify a pickup branch; inter-branch transfer becomes a copy state (IN_TRANSIT).
  • E-books with licence limits: a DigitalCopy with concurrent-licence count instead of a physical status. Loans auto-expire; no fines.
  • Lost and damaged books: mark LOST, close the loan, charge replacement cost through a separate FinePolicy or a lost-item rule.
  • Waivers and audits: fines as a ledger of transactions (charge, payment, waiver with librarian id), not a single number.
  • Notification preferences: each member picks channels; the notifier consults preferences. The service still only publishes events.

Mistakes that cost points

  • No distinction between title and copy.
  • Putting fine calculation inside Member or BookCopy.
  • Returned copies going straight back to AVAILABLE while people are waiting.
  • Holds that never expire because no actor runs time-based jobs.
  • Email-sending code inside returnCopy.
  • Using LocalDate.now() inside rules, making them untestable.
  • A Librarian extends Member hierarchy that adds nothing; roles are usually better as permissions on an account.

For more LLD problems with a similar "many rules, one home each" flavour, compare with the parking lot design, and keep the design patterns cheat sheet nearby for Observer and Strategy vocabulary.

Frequently Asked Questions

What are the main classes in a library management system?▾
Book (the title: ISBN, author, subject), BookCopy (a physical copy with a barcode and status), Member (with a membership type that sets limits), Loan (who has which copy and when it is due), a reservation queue per title, a FinePolicy, a Catalog for search, and a LibraryService facade. Notification channels plug in as observers.
What is the difference between Book and BookItem in library design?▾
Book is the abstract title shared by all copies, identified by ISBN. BookItem or BookCopy is one physical copy identified by a barcode, with its own status such as available, loaned or on hold. Loans and holds attach to copies; search and reservations attach to titles. Mixing them is the most common modelling error in this problem.
How do book reservations work in a library system?▾
A member reserves a title when no copy is available and joins a FIFO queue for that title. When any copy is returned, it is put on hold for the first member in the queue for a few days and that member is notified. If they do not collect it before the hold expires, it passes to the next member or becomes available.
How is the library fine calculated?▾
Typically a per-day rate times the number of days past the due date, possibly with a grace period and a cap, and with different rates by membership type. Put the rule behind a FinePolicy interface so the library can change it without touching the checkout and return code.
Which design patterns are used in a library management system?▾
Observer for email, SMS and in-app notifications, Strategy for fine policies and search, Facade for the LibraryService API, and sometimes State for the copy lifecycle. Factory can create members of different types.
Can a member renew a book that someone has reserved?▾
In most library policies, no. If other members are waiting for the title, renewal is refused so the queue keeps moving. Renewals are also commonly capped (for example at two) and refused for overdue books; confirm the exact rules with the interviewer.
How do you prevent two members borrowing the same copy at once?▾
Make checkout atomic on the copy. In memory, synchronise the operation or lock per copy; in a database, update the copy row with a condition on its status (or use SELECT FOR UPDATE) inside the same transaction that inserts the loan, so only one checkout can succeed.

Turn Use Cases into Clean Designs

Prepflix's LLD course trains the exact skill this problem tests: going from actors and use cases to classes, patterns and working code under interview time limits.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.