Build a Library Management System

Design a Java OOP class hierarchy to manage books, members, and borrowing.

Introduction

30 Second Summary

Libraries have rules. You can only borrow so many books at once, and you cannot take a book someone else already has. These are business rules, and every software system needs to enforce them.

In this project, you will build a console-based Library Management System in Java that manages books, registers members, and handles borrowing and returning operations with real validation rules.

What You'll Build

A fully functional Java console application with an abstract class hierarchy, collection-based data management, and a menu-driven interface that enforces business rules like borrow limits and availability checks.

By the end of this project, you'll have:

  • An abstract Book class with FictionBook and NonFictionBook subclasses demonstrating inheritance and polymorphism.
  • A Member class that tracks borrowed books with enforced limits, and a Library class that encapsulates business logic using ArrayList and HashMap.
  • A working borrow and return flow with validation rules: availability checks, a 3-book borrow limit, and duplicate prevention.
  • Secret Mission: Add a search feature that lets users find books by title keyword or author name.

Are there any prerequisites?

This project assumes intermediate Java knowledge. You should be comfortable with classes, methods, and basic syntax before starting.

Set Up Your Java Project in IntelliJ IDEA

Before you can write any library code, you need a properly structured Java project. A project gives IntelliJ IDEA the information it needs to compile and run your classes.

In this step, you will create a new project, set up a package to organise your classes, and run a quick test to confirm everything works.

In this step, get ready to:
  • Create a new Java project in IntelliJ IDEA with JDK 21.
  • Set up the package structure for your classes.
  • Verify everything compiles and runs.
Create the project
  • In IntelliJ IDEA, click New Project on the Welcome screen.
  • If you already have a project open, go to File, then New, then Project instead.
  • In the New Project wizard, select Java from the list on the left.
  • Set Name to LibrarySystem.
  • Set Build system to IntelliJ.
  • In the JDK dropdown, select the 21 entry (Oracle OpenJDK).
  • Uncheck Add sample code.
  • Click Create.

What is a build system?

The build system tells IntelliJ how to compile and package your code. The IntelliJ option uses the IDE's built-in compiler, which is the simplest choice for learning projects. Production projects often use Maven or Gradle, but those add configuration you do not need here.

IntelliJ IDEA opens your new project. You should see a src folder in the Project tool window on the left.

Create the package structure

Java uses packages to group related classes. Your library system classes will live in a single library package.

  • Open the Project tool window by pressing Alt+1 (Windows/Linux) or Cmd+1 (macOS).
  • Right-click the src folder.
  • Select New, then Package.
  • Type library and press Enter.

You should see a library folder appear inside src in the Project tool window.

Don't see the package?

Make sure you right-clicked the src folder (marked in blue), not the top-level project folder. If you accidentally created the package in the wrong place, right-click it, select Delete, and try again on the src folder.

Help me fix my package structure in IntelliJ

Verify the setup
  • Right-click the library package in the Project tool window.
  • Select New, then Java Class.
  • Type Main and press Enter.

IntelliJ creates Main.java with the package declaration and an empty class body already in place.

  • Replace the entire contents of Main.java with the following code:
package library;

public class Main {
    public static void main(String[] args) {
        System.out.println("Library System Starting...");
    }
}

What does this code do?

package library; declares that this class belongs to the library package you just created.

public static void main(String[] args) is the entry point Java looks for when you run a program. The println line prints a message to confirm everything is wired up correctly.

  • Run the program by pressing Shift+F10 (Windows/Linux) or Ctrl+R (macOS).
  • You can also click the green play icon in the gutter next to the main method and select Run 'Main.main()'.
  • You should see Library System Starting... printed in the Run window at the bottom of IntelliJ IDEA.

Don't see the output?

If you see a red error message instead, check that you typed Main with a capital M when you created the class. Java class names are case-sensitive and must match the file name exactly.

If the green play icon is missing from the gutter, make sure your main method signature matches public static void main(String[] args) exactly.

Help me fix my Main class in IntelliJ

✔️ Awesome, I've got everything!

Great. Make sure you have saved your file (IntelliJ auto-saves, so you should be good).

ⓧ I'd like to double check the full code

Here is the complete Main.java you should have at this point:

package library;

public class Main {
    public static void main(String[] args) {
        System.out.println("Library System Starting...");
    }
}

Your project is set up and running. Next up, you will build the Book class hierarchy that powers your library's catalog.

Build the Book Class Hierarchy

Your project is set up and running in IntelliJ IDEA. Now it's time to model the core domain object of the system: books.

A real library has different types of books like fiction and non-fiction. You can model this shared-but-different relationship using an abstract base class with concrete subclasses, bringing inheritance and polymorphism to life.

In this step, get ready to:
  • Create an abstract Book class with shared fields and behavior.
  • Build FictionBook and NonFictionBook subclasses with unique attributes.
  • Test polymorphism by looping through a List<Book>.
Create the abstract Book class

The Book class defines the fields and behavior that all books share: a title, an author, an ISBN, and an availability status. Marking the class abstract means you can never create a plain Book object directly. Every book must be a specific type.

  • In the Project tool window (Alt+1), right-click the library package and select New | Java Class.
  • Name it Book and press Enter.
  • Replace the generated code with the following:
package library;

public abstract class Book {
    private String title;
    private String author;
    private String isbn;
    private boolean isAvailable;

    // Constructor sets basic book info and marks it as available
    public Book(String title, String author, String isbn) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
        this.isAvailable = true;
    }

    // Getters for accessing private fields
    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }

    public String getIsbn() {
        return isbn;
    }

    public boolean isAvailable() {
        return isAvailable;
    }

    // Setter to change availability when borrowed or returned
    public void setAvailable(boolean available) {
        this.isAvailable = available;
    }

    // Subclasses must implement this to identify their category
    public abstract String getCategory();

    // Displays book info including category from the subclass
    @Override
    public String toString() {
        return getCategory() + " | " + title + " by " + author + " (ISBN: " + isbn + ") - " + (isAvailable ? "Available" : "Borrowed");
    }
}

What does this code do?

The four private fields (title, author, isbn, isAvailable) are encapsulated. External code accesses them only through getters and the setter.

The abstract keyword on getCategory() forces every subclass to provide its own implementation. The toString() method calls getCategory() at runtime, so each subclass prints its own category without this base class knowing the details.

  • Save Book.java with Ctrl+S (Windows) or Cmd+S (macOS).
  • Check the editor gutter for any red error icons. You should see none. IntelliJ may show a warning that the class is never used, which is expected for now.

Seeing a red squiggly line?

Make sure the class declaration says public abstract class Book with both keywords present. If you see an error about the package statement, confirm the first line reads package library; and that the file lives inside the library folder under src.

Help me fix a compile error in my abstract Book class

✔️ Awesome, I've got everything!

Great. Make sure you have saved Book.java before moving on.

ⓧ I'd like to double check the full code

package library;

public abstract class Book {
    private String title;
    private String author;
    private String isbn;
    private boolean isAvailable;

    public Book(String title, String author, String isbn) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
        this.isAvailable = true;
    }

    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }

    public String getIsbn() {
        return isbn;
    }

    public boolean isAvailable() {
        return isAvailable;
    }

    public void setAvailable(boolean available) {
        this.isAvailable = available;
    }

    public abstract String getCategory();

    @Override
    public String toString() {
        return getCategory() + " | " + title + " by " + author + " (ISBN: " + isbn + ") - " + (isAvailable ? "Available" : "Borrowed");
    }
}
Create the FictionBook and NonFictionBook subclasses

Each subclass extends Book and adds its own unique field. FictionBook adds a genre. NonFictionBook adds a subject.

  • Right-click the library package and select New | Java Class.
  • Name it FictionBook and press Enter.
  • Replace the generated code with the following:
package library;

public class FictionBook extends Book {
    private String genre;

    // Constructor passes shared fields to Book, then sets genre
    public FictionBook(String title, String author, String isbn, String genre) {
        super(title, author, isbn);
        this.genre = genre;
    }

    public String getGenre() {
        return genre;
    }

    // Implements the abstract method from Book
    @Override
    public String getCategory() {
        return "Fiction";
    }

    // Appends genre info to the base toString output
    @Override
    public String toString() {
        return super.toString() + " [Genre: " + genre + "]";
    }
}

What does this code do?

The super(title, author, isbn) call passes the shared fields up to the Book constructor. The FictionBook only manages its own genre field.

By overriding getCategory() to return "Fiction", this class fulfills the contract the abstract class defined. The toString() override calls super.toString() to get the base info and then appends the genre.

  • Save FictionBook.java.
  • Confirm there are no red error icons in the gutter. The file should compile cleanly.

Seeing 'cannot resolve symbol Book'?

Make sure the first line of FictionBook.java says package library; and that Book.java also has the same package statement. Both files must be in the library package for the extends Book to resolve.

Help me fix 'cannot resolve symbol' in my FictionBook class

✔️ Awesome, I've got everything!

Great. Make sure you have saved FictionBook.java before moving on.

ⓧ I'd like to double check the full code

package library;

public class FictionBook extends Book {
    private String genre;

    public FictionBook(String title, String author, String isbn, String genre) {
        super(title, author, isbn);
        this.genre = genre;
    }

    public String getGenre() {
        return genre;
    }

    @Override
    public String getCategory() {
        return "Fiction";
    }

    @Override
    public String toString() {
        return super.toString() + " [Genre: " + genre + "]";
    }
}
  • Right-click the library package again and select New | Java Class.
  • Name it NonFictionBook and press Enter.
  • Replace the generated code with the following:
package library;

public class NonFictionBook extends Book {
    private String subject;

    // Constructor passes shared fields to Book, then sets subject
    public NonFictionBook(String title, String author, String isbn, String subject) {
        super(title, author, isbn);
        this.subject = subject;
    }

    public String getSubject() {
        return subject;
    }

    // Implements the abstract method from Book
    @Override
    public String getCategory() {
        return "Non-Fiction";
    }

    // Appends subject info to the base toString output
    @Override
    public String toString() {
        return super.toString() + " [Subject: " + subject + "]";
    }
}

What does this code do?

This follows the same pattern as FictionBook. The only differences are the unique field (subject instead of genre) and getCategory() returning "Non-Fiction".

Both subclasses share the same base fields and toString() logic from Book, but each adds its own category and extra detail. This is inheritance at work.

  • Save NonFictionBook.java.
  • Confirm there are no red error icons. Your library package should now contain four files: Book.java, FictionBook.java, Main.java, and NonFictionBook.java.

Don't see all four files in the package?

Expand the library package in the Project tool window. If a file is missing, right-click the library package and create it again with New | Java Class. Make sure you right-click the library package itself, not the src folder.

Help me find my missing class files in IntelliJ

✔️ Awesome, I've got everything!

Great. Make sure you have saved NonFictionBook.java before moving on.

ⓧ I'd like to double check the full code

package library;

public class NonFictionBook extends Book {
    private String subject;

    public NonFictionBook(String title, String author, String isbn, String subject) {
        super(title, author, isbn);
        this.subject = subject;
    }

    public String getSubject() {
        return subject;
    }

    @Override
    public String getCategory() {
        return "Non-Fiction";
    }

    @Override
    public String toString() {
        return super.toString() + " [Subject: " + subject + "]";
    }
}
Test polymorphism in Main

Polymorphism means you can treat both FictionBook and NonFictionBook as Book objects. When you call toString() on each one, Java automatically uses the correct subclass version at runtime.

  • Open Main.java in the editor by double-clicking it in the Project tool window.
  • Replace all the code in Main.java with the following:
package library;

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // Store different book types in a single List<Book>
        List<Book> books = new ArrayList<>();
        books.add(new FictionBook("Dune", "Frank Herbert", "978-0441172719", "Sci-Fi"));
        books.add(new NonFictionBook("Sapiens", "Yuval Noah Harari", "978-0062316097", "History"));

        // Polymorphism: each book prints its own category and details
        for (Book book : books) {
            System.out.println(book);
        }
    }
}

What does this code do?

The List<Book> holds references of type Book, but each element is actually a FictionBook or NonFictionBook. When System.out.println(book) runs, Java calls the overridden toString() from the actual subclass, not the base class.

This is polymorphism in action. The loop does not need to know which specific type each book is. Java resolves the correct method at runtime.

  • Save Main.java.
  • Run the program by pressing Shift+F10 (or right-click Main.java and select Run 'Main.main()').
  • In the Run window at the bottom, you should see two lines of output. The first line starts with Fiction | and shows Dune's details with [Genre: Sci-Fi]. The second starts with Non-Fiction | and shows Sapiens with [Subject: History].

Seeing 'cannot find symbol' errors?

This usually means one of the class files has a typo in its name or is in the wrong package. Check that FictionBook.java and NonFictionBook.java both start with package library; and that the class names match the file names exactly (including capitalization).

Also check that you imported java.util.ArrayList and java.util.List at the top of Main.java. Missing imports cause 'cannot find symbol' for ArrayList and List.

Help me debug 'cannot find symbol' in my Main class

✔️ Awesome, I've got everything!

Great. Double check that you have saved all files before moving on.

ⓧ I'd like to double check the full code

package library;

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Book> books = new ArrayList<>();
        books.add(new FictionBook("Dune", "Frank Herbert", "978-0441172719", "Sci-Fi"));
        books.add(new NonFictionBook("Sapiens", "Yuval Noah Harari", "978-0062316097", "History"));
        for (Book book : books) {
            System.out.println(book);
        }
    }
}

Why use an abstract class here?

You never want someone to create a generic Book with no category. Every book in a real library is either fiction or non-fiction. The abstract keyword enforces this at compile time. If anyone tries to write new Book(...), Java refuses to compile it.

This is the same pattern you will see in production Java code when a base type should never be instantiated directly.

Your class hierarchy is complete and polymorphism is working. Next up, you will create a Member class and a Library class that ties everything together into a functioning system.

Create the Member Class and Library Class

Your Book class hierarchy is working. Polymorphism lets you treat FictionBook and NonFictionBook as a single Book type, and each subclass provides its own behavior.

A library is more than just books. It needs members who can borrow them, and a central system that manages both the catalog and the member registry. In this step, you will build a Member class to represent borrowers and a Library class that ties everything together.

In this step, get ready to:
  • Create a Member class that tracks borrowed books.
  • Create a Library class that manages the full catalog and member registry.
  • Wire both classes into Main to verify the output.
Create the Member class

A member needs a name, an ID, and a way to track which books they currently have. The Member class uses an ArrayList to store a dynamic list of borrowed books that can grow and shrink as the member borrows and returns.

  • In the Project tool window, right-click the library package.
  • Select New | Java Class and name it Member.
  • Replace the contents of Member.java with the following code:
package library;

import java.util.ArrayList;
import java.util.List;

public class Member {
    private String name;
    private String memberId;
    private List<Book> borrowedBooks;

    public Member(String name, String memberId) {
        this.name = name;
        this.memberId = memberId;
        this.borrowedBooks = new ArrayList<>();
    }

    public String getName() {
        return name;
    }

    public String getMemberId() {
        return memberId;
    }

    public List<Book> getBorrowedBooks() {
        return borrowedBooks;
    }

    // Adds a book to this member's borrowed list
    public void borrowBook(Book book) {
        borrowedBooks.add(book);
    }

    // Removes a book from this member's borrowed list
    public void returnBook(Book book) {
        borrowedBooks.remove(book);
    }

    // Returns the current number of borrowed books
    public int getBorrowedCount() {
        return borrowedBooks.size();
    }

    @Override
    public String toString() {
        return name + " (ID: " + memberId + ") - Books borrowed: " + getBorrowedCount();
    }
}

What does this code do?

  • Three private fields (name, memberId, borrowedBooks) keep member data encapsulated inside the class.
  • The constructor initializes borrowedBooks as an empty ArrayList. Every new member starts with zero borrowed books.
  • borrowBook() and returnBook() modify the list. getBorrowedCount() returns the list size so other code can check limits without accessing the list directly.

Why use private fields with getters?

This is encapsulation in action. External code cannot change a member's name or manipulate the borrowed list in unexpected ways. All modifications go through defined methods like borrowBook(), which means you can add validation logic later without changing any calling code.

  • Save Member.java.
  • Check that IntelliJ shows no red underlines in the editor. The file should compile without errors.

Seeing a red underline on Book?

Make sure the package library; line is at the top of the file. Both Member.java and Book.java must be in the same package for the reference to resolve.

help me fix the Book reference error in my Member class

✔️ Awesome, I've got everything!

Great. Make sure you have saved Member.java before moving on.

ⓧ I'd like to double check the full code

package library;

import java.util.ArrayList;
import java.util.List;

public class Member {
    private String name;
    private String memberId;
    private List<Book> borrowedBooks;

    public Member(String name, String memberId) {
        this.name = name;
        this.memberId = memberId;
        this.borrowedBooks = new ArrayList<>();
    }

    public String getName() {
        return name;
    }

    public String getMemberId() {
        return memberId;
    }

    public List<Book> getBorrowedBooks() {
        return borrowedBooks;
    }

    public void borrowBook(Book book) {
        borrowedBooks.add(book);
    }

    public void returnBook(Book book) {
        borrowedBooks.remove(book);
    }

    public int getBorrowedCount() {
        return borrowedBooks.size();
    }

    @Override
    public String toString() {
        return name + " (ID: " + memberId + ") - Books borrowed: " + getBorrowedCount();
    }
}
Create the Library class

The Library class is the central manager. It holds two collections: an ArrayList for the full book catalog and a HashMap for members keyed by their ID.

Why use a HashMap for members?

A HashMap lets you look up a member instantly by their ID string. Without it, you would have to loop through every member each time you need to find one. This becomes important when the borrow and return logic needs to locate a member quickly.

  • Right-click the library package, select New | Java Class, and name it Library.
  • Replace the contents of Library.java with the following code:
package library;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class Library {
    // The full catalog of books
    private List<Book> books;
    // Members keyed by memberId for fast lookup
    private HashMap<String, Member> members;

    public Library() {
        this.books = new ArrayList<>();
        this.members = new HashMap<>();
    }

    public void addBook(Book book) {
        books.add(book);
        System.out.println("Book added: " + book.getTitle());
    }

    public void addMember(Member member) {
        members.put(member.getMemberId(), member);
        System.out.println("Member added: " + member.getName());
    }

    public void displayAllBooks() {
        System.out.println("\n--- All Books ---");
        if (books.isEmpty()) {
            System.out.println("No books in the library.");
            return;
        }
        for (int i = 0; i < books.size(); i++) {
            System.out.println((i + 1) + ". " + books.get(i));
        }
    }

    public void displayAllMembers() {
        System.out.println("\n--- All Members ---");
        if (members.isEmpty()) {
            System.out.println("No members registered.");
            return;
        }
        for (Member member : members.values()) {
            System.out.println(member);
        }
    }

    public Book findBookByIsbn(String isbn) {
        for (Book book : books) {
            if (book.getIsbn().equals(isbn)) {
                return book;
            }
        }
        return null;
    }
}

What does this code do?

  • addBook() and addMember() store items in the internal collections and print a confirmation. Outside code never touches the collections directly.
  • displayAllBooks() loops through the book list with a numbered index. The empty check prevents printing a blank section when the catalog is empty.
  • displayAllMembers() iterates over the HashMap's values. Since a HashMap is not indexed by position, it uses a for-each loop over members.values().
  • findBookByIsbn() searches the list and returns the matching Book, or null if nothing matches. You will use this in the next step for borrow and return logic.
  • Save Library.java.
  • Check that IntelliJ shows no red underlines. All references to Book and Member should resolve cleanly.

Seeing errors on Member or HashMap?

Make sure the three import statements (java.util.ArrayList, java.util.HashMap, java.util.List) are at the top of the file. Also confirm that Member.java was saved in the same library package.

help me fix import or reference errors in Library.java

✔️ Awesome, I've got everything!

Great. Make sure you have saved Library.java before moving on.

ⓧ I'd like to double check the full code

package library;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class Library {
    private List<Book> books;
    private HashMap<String, Member> members;

    public Library() {
        this.books = new ArrayList<>();
        this.members = new HashMap<>();
    }

    public void addBook(Book book) {
        books.add(book);
        System.out.println("Book added: " + book.getTitle());
    }

    public void addMember(Member member) {
        members.put(member.getMemberId(), member);
        System.out.println("Member added: " + member.getName());
    }

    public void displayAllBooks() {
        System.out.println("\n--- All Books ---");
        if (books.isEmpty()) {
            System.out.println("No books in the library.");
            return;
        }
        for (int i = 0; i < books.size(); i++) {
            System.out.println((i + 1) + ". " + books.get(i));
        }
    }

    public void displayAllMembers() {
        System.out.println("\n--- All Members ---");
        if (members.isEmpty()) {
            System.out.println("No members registered.");
            return;
        }
        for (Member member : members.values()) {
            System.out.println(member);
        }
    }

    public Book findBookByIsbn(String isbn) {
        for (Book book : books) {
            if (book.getIsbn().equals(isbn)) {
                return book;
            }
        }
        return null;
    }
}
Wire it together in Main

Now you will update Main.java to create a Library instance, load it with sample data, and display the results.

  • Open Main.java in the editor.
  • Replace the entire contents with the following code:
package library;

public class Main {
    public static void main(String[] args) {
        // Create the central library instance
        Library library = new Library();

        // Add sample books (mix of Fiction and Non-Fiction)
        library.addBook(new FictionBook("The Great Gatsby", "F. Scott Fitzgerald", "978-0743273565", "Classic"));
        library.addBook(new FictionBook("Dune", "Frank Herbert", "978-0441172719", "Sci-Fi"));
        library.addBook(new NonFictionBook("Sapiens", "Yuval Noah Harari", "978-0062316097", "History"));
        library.addBook(new NonFictionBook("Clean Code", "Robert C. Martin", "978-0132350884", "Programming"));

        // Register sample members
        library.addMember(new Member("Alice Johnson", "M001"));
        library.addMember(new Member("Bob Smith", "M002"));

        // Display the full catalog and member list
        library.displayAllBooks();
        library.displayAllMembers();
    }
}

What does this code do?

  • A single Library object now owns all the data. You no longer manage loose lists yourself.
  • addBook() accepts any Book subclass thanks to polymorphism. You pass in FictionBook and NonFictionBook objects, and the library stores them all as Book.
  • displayAllBooks() and displayAllMembers() print formatted output using each object's toString() method.
  • Save Main.java.
  • Run the program by pressing Shift+F10.
  • In the Run window, you should see confirmation messages for each book and member added, followed by the full book list (with categories and availability) and the member list.

Seeing a compilation error?

If IntelliJ reports that it cannot find Library or Member, confirm both files are saved inside the library package (not in the src root). Each file must have package library; as its first line.

If you removed the old imports (java.util.ArrayList and java.util.List) from Main.java but still see errors, make sure the entire file matches the code above. The new version does not need those imports.

help me fix my Main.java compilation errors

✔️ Awesome, I've got everything!

Great. Double check you have saved all files.

ⓧ I'd like to double check the full code

package library;

public class Main {
    public static void main(String[] args) {
        Library library = new Library();

        library.addBook(new FictionBook("The Great Gatsby", "F. Scott Fitzgerald", "978-0743273565", "Classic"));
        library.addBook(new FictionBook("Dune", "Frank Herbert", "978-0441172719", "Sci-Fi"));
        library.addBook(new NonFictionBook("Sapiens", "Yuval Noah Harari", "978-0062316097", "History"));
        library.addBook(new NonFictionBook("Clean Code", "Robert C. Martin", "978-0132350884", "Programming"));

        library.addMember(new Member("Alice Johnson", "M001"));
        library.addMember(new Member("Bob Smith", "M002"));

        library.displayAllBooks();
        library.displayAllMembers();
    }
}

Your library has a catalog and a member registry. Next up, you will add the real action: borrow and return logic with business rules that prevent members from taking too many books.

Implement Borrow and Return Logic

Your library has books and members, but nobody can actually borrow anything yet. A real library enforces rules: you cannot borrow a book someone else already has, and you cannot keep borrowing indefinitely.

In this step, you will add borrow and return methods to the Library class that enforce business rules. These methods demonstrate encapsulation in action: all the validation logic lives inside one class, and callers simply pass in an ID.

In this step, get ready to:
  • Add borrow logic with validation rules to the Library class.
  • Add return logic that restores book availability.
  • Test that business rules reject invalid operations with specific error messages.
Add borrow logic to Library

The borrowBook method needs to enforce three rules before allowing a borrow: the book must exist and be available, the member must exist, and the member must not have already hit the 3-book limit.

  • In Library.java, add the following method below the findBookByIsbn method:
    public void borrowBook(String memberId, String isbn) {
        // Look up the member by their ID
        Member member = members.get(memberId);
        if (member == null) {
            System.out.println("Error: Member not found with ID: " + memberId);
            return;
        }

        // Look up the book by its ISBN
        Book book = findBookByIsbn(isbn);
        if (book == null) {
            System.out.println("Error: Book not found with ISBN: " + isbn);
            return;
        }

        // Check if the book is currently available
        if (!book.isAvailable()) {
            System.out.println("Error: '" + book.getTitle() + "' is currently not available.");
            return;
        }

        // Enforce the 3-book borrow limit
        if (member.getBorrowedCount() >= 3) {
            System.out.println("Error: " + member.getName() + " has already borrowed 3 books (maximum limit).");
            return;
        }

        // All checks passed: mark book as borrowed
        book.setAvailable(false);
        member.borrowBook(book);
        System.out.println("Success: " + member.getName() + " borrowed '" + book.getTitle() + "'");
    }

What does this code do?

This method uses an early-return pattern. Each validation check returns immediately with a specific error message if it fails. Only when all four checks pass does the code reach the success path.

  • members.get(memberId) uses the HashMap for fast O(1) lookup instead of looping through all members.
  • findBookByIsbn(isbn) reuses the helper method you wrote in the previous step.
  • The 3-book limit check uses getBorrowedCount() >= 3 to enforce the maximum.
  • Save Library.java.

You will not see a change yet. The next chunk adds the return method, and then you will test both together.

Seeing a red underline?

  • Make sure the method is placed inside the Library class body (before the final closing brace }).
  • Check that the method signature uses String memberId, String isbn as parameters. Typos in parameter types cause compile errors.

Help me fix a compile error in my borrowBook method.

Add return logic to Library

The return method mirrors the borrow logic. It validates that the member and book exist, and crucially checks that the member actually has that book before accepting the return.

  • In Library.java, add the following method below the borrowBook method you just wrote:
    public void returnBook(String memberId, String isbn) {
        // Look up the member
        Member member = members.get(memberId);
        if (member == null) {
            System.out.println("Error: Member not found with ID: " + memberId);
            return;
        }

        // Look up the book
        Book book = findBookByIsbn(isbn);
        if (book == null) {
            System.out.println("Error: Book not found with ISBN: " + isbn);
            return;
        }

        // Verify the member actually borrowed this book
        if (!member.getBorrowedBooks().contains(book)) {
            System.out.println("Error: " + member.getName() + " has not borrowed '" + book.getTitle() + "'");
            return;
        }

        // All checks passed: mark book as available again
        book.setAvailable(true);
        member.returnBook(book);
        System.out.println("Success: " + member.getName() + " returned '" + book.getTitle() + "'");
    }

What does this code do?

  • member.getBorrowedBooks().contains(book) checks whether the member's borrowed list actually contains this specific book object. This prevents returning a book you never borrowed.
  • book.setAvailable(true) flips the book back to available so other members can borrow it.
  • member.returnBook(book) removes the book from the member's personal borrowed list.
  • Save Library.java.

You will not see output yet. The next chunk updates Main.java to exercise both methods.

Red underline on getBorrowedBooks()?

  • Make sure Member.java has a public getBorrowedBooks() method that returns List<Book>. If you skipped that getter in Step 3, add it now.
  • Confirm the method is spelled exactly getBorrowedBooks (capital B twice). Java method names are case-sensitive.

Help me fix a compile error in my returnBook method.

✔️ Awesome, I've got everything!

Great. Double check you have saved Library.java before moving on.

ⓧ I'd like to double check the full code

package library;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class Library {
    private List<Book> books;
    private HashMap<String, Member> members;

    public Library() {
        this.books = new ArrayList<>();
        this.members = new HashMap<>();
    }

    public void addBook(Book book) {
        books.add(book);
        System.out.println("Book added: " + book.getTitle());
    }

    public void addMember(Member member) {
        members.put(member.getMemberId(), member);
        System.out.println("Member added: " + member.getName());
    }

    public void displayAllBooks() {
        System.out.println("\n--- All Books ---");
        if (books.isEmpty()) {
            System.out.println("No books in the library.");
            return;
        }
        for (int i = 0; i < books.size(); i++) {
            System.out.println((i + 1) + ". " + books.get(i));
        }
    }

    public void displayAllMembers() {
        System.out.println("\n--- All Members ---");
        if (members.isEmpty()) {
            System.out.println("No members registered.");
            return;
        }
        for (Member member : members.values()) {
            System.out.println(member);
        }
    }

    public Book findBookByIsbn(String isbn) {
        for (Book book : books) {
            if (book.getIsbn().equals(isbn)) {
                return book;
            }
        }
        return null;
    }

    public void borrowBook(String memberId, String isbn) {
        Member member = members.get(memberId);
        if (member == null) {
            System.out.println("Error: Member not found with ID: " + memberId);
            return;
        }

        Book book = findBookByIsbn(isbn);
        if (book == null) {
            System.out.println("Error: Book not found with ISBN: " + isbn);
            return;
        }

        if (!book.isAvailable()) {
            System.out.println("Error: '" + book.getTitle() + "' is currently not available.");
            return;
        }

        if (member.getBorrowedCount() >= 3) {
            System.out.println("Error: " + member.getName() + " has already borrowed 3 books (maximum limit).");
            return;
        }

        book.setAvailable(false);
        member.borrowBook(book);
        System.out.println("Success: " + member.getName() + " borrowed '" + book.getTitle() + "'");
    }

    public void returnBook(String memberId, String isbn) {
        Member member = members.get(memberId);
        if (member == null) {
            System.out.println("Error: Member not found with ID: " + memberId);
            return;
        }

        Book book = findBookByIsbn(isbn);
        if (book == null) {
            System.out.println("Error: Book not found with ISBN: " + isbn);
            return;
        }

        if (!member.getBorrowedBooks().contains(book)) {
            System.out.println("Error: " + member.getName() + " has not borrowed '" + book.getTitle() + "'");
            return;
        }

        book.setAvailable(true);
        member.returnBook(book);
        System.out.println("Success: " + member.getName() + " returned '" + book.getTitle() + "'");
    }
}
Test the business rules

The best way to verify your validation logic is to deliberately trigger failures. You will borrow a book, try to borrow it again (triggering the availability check), and then return it to confirm availability is restored.

  • In Main.java, find these two lines at the bottom of the main method:
        library.displayAllBooks();
        library.displayAllMembers();
  • Replace those two lines with the following test code:
        // Test borrow (should succeed)
        library.borrowBook("M001", "978-0441172719");
        // Test borrow again (should fail: not available)
        library.borrowBook("M002", "978-0441172719");
        // Test return (should succeed)
        library.returnBook("M001", "978-0441172719");

What does this test?

  • The first call borrows "Dune" for Alice (member M001). This should print a success message.
  • The second call tries to borrow the same book for Bob (member M002). Since Alice already has it, the availability check should reject this with an error.
  • The third call returns the book from Alice. This should succeed and mark the book as available again.
  • Save Main.java.
  • Run the program by pressing Shift+F10.

In the Run window, you should see the "Book added" and "Member added" lines from setup, followed by three test results:

  • Success: Alice Johnson borrowed 'Dune'
  • Error: 'Dune' is currently not available.
  • Success: Alice Johnson returned 'Dune'

Not seeing the expected output?

  • If you see all three as "Success", make sure the second call uses a different member ID (M002) for the same ISBN. That is what triggers the "not available" error.
  • If you see "Member not found" or "Book not found", check that the member IDs (M001, M002) and ISBN (978-0441172719) match what you used in your addMember and addBook calls.
  • If you see a compile error, make sure you deleted the old displayAllBooks() and displayAllMembers() lines completely before pasting the test code.

Help me debug my borrow/return test output.

✔️ Awesome, I've got everything!

Great. Double check you have saved Main.java before moving on.

ⓧ I'd like to double check the full code

package library;

public class Main {
    public static void main(String[] args) {
        Library library = new Library();

        library.addBook(new FictionBook("The Great Gatsby", "F. Scott Fitzgerald", "978-0743273565", "Classic"));
        library.addBook(new FictionBook("Dune", "Frank Herbert", "978-0441172719", "Sci-Fi"));
        library.addBook(new NonFictionBook("Sapiens", "Yuval Noah Harari", "978-0062316097", "History"));
        library.addBook(new NonFictionBook("Clean Code", "Robert C. Martin", "978-0132350884", "Programming"));

        library.addMember(new Member("Alice Johnson", "M001"));
        library.addMember(new Member("Bob Smith", "M002"));

        // Test borrow (should succeed)
        library.borrowBook("M001", "978-0441172719");
        // Test borrow again (should fail: not available)
        library.borrowBook("M002", "978-0441172719");
        // Test return (should succeed)
        library.returnBook("M001", "978-0441172719");
    }
}

Why encapsulate business rules inside Library?

Notice how Main.java only passes in two strings (a member ID and an ISBN). It does not check availability or enforce limits. All that logic is hidden inside the Library class.

This is encapsulation at work. If you later change the borrow limit from 3 to 5, you update one place and every caller benefits automatically.

Your library now enforces real business rules. Next up, you will replace this test code with a full interactive menu so users can borrow, return, and add books themselves.

Build the Console Menu Interface

Your Library class handles all the business logic for borrowing and returning books. But right now, the only way to use it is by hardcoding test calls in Main.java.

In this step, you will build an interactive console menu that lets users choose actions from a numbered list. This is how real command-line applications work: a loop presents options, reads input, and dispatches to the correct logic.

In this step, get ready to:
  • Replace the test code in Main with a menu-driven loop using Scanner.
  • Implement interactive prompts for borrowing, returning, and adding books.
  • Pre-populate the library with sample data so users can test immediately.
Create the menu loop

The Scanner class reads user input from the console. You will use it to capture menu selections and text input for borrowing, returning, and adding books.

  • Open Main.java in the editor.
  • Select all the existing code (Ctrl+A on Windows, Cmd+A on macOS) and delete it.
  • Replace it with the following code that sets up the library, pre-populates sample data, and prints the system header:
package library;

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Library library = new Library();
        Scanner scanner = new Scanner(System.in);

        library.addBook(new FictionBook("The Great Gatsby", "F. Scott Fitzgerald", "978-0743273565", "Classic"));
        library.addBook(new FictionBook("Dune", "Frank Herbert", "978-0441172719", "Sci-Fi"));
        library.addBook(new NonFictionBook("Sapiens", "Yuval Noah Harari", "978-0062316097", "History"));
        library.addBook(new NonFictionBook("Clean Code", "Robert C. Martin", "978-0132350884", "Programming"));

        library.addMember(new Member("Alice Johnson", "M001"));
        library.addMember(new Member("Bob Smith", "M002"));

        System.out.println("\n=== Library Management System ===");

        boolean running = true;
        while (running) {
            System.out.println("\n--- Menu ---");
            System.out.println("1. Display All Books");
            System.out.println("2. Display All Members");
            System.out.println("3. Borrow a Book");
            System.out.println("4. Return a Book");
            System.out.println("5. Add a Book");
            System.out.println("6. Exit");
            System.out.print("Choose an option: ");

            int choice = scanner.nextInt();
            scanner.nextLine();

            switch (choice) {
                case 1:
                    library.displayAllBooks();
                    break;
                case 2:
                    library.displayAllMembers();
                    break;
                case 3:
                    System.out.print("Enter Member ID: ");
                    String borrowMemberId = scanner.nextLine();
                    System.out.print("Enter Book ISBN: ");
                    String borrowIsbn = scanner.nextLine();
                    library.borrowBook(borrowMemberId, borrowIsbn);
                    break;
                case 4:
                    System.out.print("Enter Member ID: ");
                    String returnMemberId = scanner.nextLine();
                    System.out.print("Enter Book ISBN: ");
                    String returnIsbn = scanner.nextLine();
                    library.returnBook(returnMemberId, returnIsbn);
                    break;
                case 5:
                    System.out.print("Enter title: ");
                    String title = scanner.nextLine();
                    System.out.print("Enter author: ");
                    String author = scanner.nextLine();
                    System.out.print("Enter ISBN: ");
                    String isbn = scanner.nextLine();
                    System.out.print("Type (1 for Fiction, 2 for Non-Fiction): ");
                    int type = scanner.nextInt();
                    scanner.nextLine();
                    if (type == 1) {
                        System.out.print("Enter genre: ");
                        String genre = scanner.nextLine();
                        library.addBook(new FictionBook(title, author, isbn, genre));
                    } else {
                        System.out.print("Enter subject: ");
                        String subject = scanner.nextLine();
                        library.addBook(new NonFictionBook(title, author, isbn, subject));
                    }
                    break;
                case 6:
                    running = false;
                    System.out.println("Goodbye!");
                    break;
                default:
                    System.out.println("Invalid option. Please try again.");
            }
        }

        scanner.close();
    }
}

What does this code do?

This is the full interactive menu system. Here is how it breaks down:

  • Scanner scanner = new Scanner(System.in) creates a reader that captures keyboard input from the console.
  • The while (running) loop keeps the menu visible until the user explicitly selects Exit (option 6).
  • scanner.nextInt() reads the number the user types. The following scanner.nextLine() consumes the leftover newline character so future reads work correctly.
  • The switch statement routes each menu option to the correct logic.
  • Save Main.java.
  • Run the program (Shift+F10). You should see the system header === Library Management System === followed by the menu options 1 through 6.

Menu not appearing?

  • Check that the import java.util.Scanner; line is at the top of the file. Without it, the compiler does not know what Scanner is.
  • Make sure you deleted ALL the old test code. Leftover lines from the previous version can cause compile errors.
  • Confirm the file still says package library; as the very first line.

Help me debug my Main.java menu loop

Implement each menu option

The menu is running. Now take a closer look at how the interactive options work.

How do the Borrow, Return, and Add options work?

Cases 3, 4, and 5 each prompt the user for input before calling the corresponding Library method:

  • Borrow a Book (case 3): asks for a Member ID and ISBN, then calls library.borrowBook(). The Library class handles all validation (availability, borrow limit).
  • Return a Book (case 4): asks for the same inputs and calls library.returnBook(). The method checks that the member actually has the book.
  • Add a Book (case 5): collects title, author, and ISBN first. Then it asks whether the book is Fiction or Non-Fiction and collects the extra field (genre or subject) before creating the correct subclass.

Why is there a scanner.nextLine() after scanner.nextInt()?

When nextInt() reads a number, it leaves the newline character (from pressing Enter) in the input buffer. If you call nextLine() next without consuming that leftover newline, it reads an empty string instead of waiting for user input.

The extra scanner.nextLine() after every nextInt() call clears that leftover newline so subsequent reads behave correctly.

  • In the Run console, type 3 and press Enter to select Borrow a Book.
  • Enter M001 for the Member ID.
  • Enter 978-0441172719 for the ISBN.

You should see: Success: Alice Johnson borrowed 'Dune'.

  • Now select option 1 to Display All Books.

Dune should now show as Borrowed instead of Available.

Program skipping your input?

  • If the program skips past a prompt without waiting for you to type, check that you have scanner.nextLine(); immediately after every scanner.nextInt(); call. This appears twice in the code: once after reading the menu choice, and once after reading the book type in case 5.
  • If you see an error about member or book not found, double-check you typed the ID or ISBN exactly as shown (IDs are case-sensitive).

Help me fix my Scanner input issue

Add sample data on startup

The code pre-loads 4 books and 2 members before the menu loop starts. This means users can immediately test borrowing and returning without manually adding data first.

Why pre-populate the library?

A library with no books or members is not testable. Pre-populating gives the user something to interact with the moment they launch the program. The mix of FictionBook and NonFictionBook instances also demonstrates polymorphism in the display output.

  • Run the program again (Shift+F10) to start fresh.
  • Select option 1 to Display All Books. You should see 4 books listed with their category, author, ISBN, availability status, and the extra field (genre or subject).
  • Select option 4 to Return a Book. Enter M001 and 978-0441172719. You should see an error because Alice has not borrowed that book yet.
  • Select option 5 to Add a Book. Enter any title, author, and ISBN. Choose 1 for Fiction and provide a genre. Then Display All Books again to confirm it was added.
  • Select option 6 to Exit. You should see Goodbye! and the program should stop.

✔️ Awesome, I've got everything!

Great. Double check you have saved Main.java and your menu is working interactively.

ⓧ I'd like to double check the full code

Here is the complete Main.java file. Compare it against yours line by line:

package library;

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Library library = new Library();
        Scanner scanner = new Scanner(System.in);

        library.addBook(new FictionBook("The Great Gatsby", "F. Scott Fitzgerald", "978-0743273565", "Classic"));
        library.addBook(new FictionBook("Dune", "Frank Herbert", "978-0441172719", "Sci-Fi"));
        library.addBook(new NonFictionBook("Sapiens", "Yuval Noah Harari", "978-0062316097", "History"));
        library.addBook(new NonFictionBook("Clean Code", "Robert C. Martin", "978-0132350884", "Programming"));

        library.addMember(new Member("Alice Johnson", "M001"));
        library.addMember(new Member("Bob Smith", "M002"));

        System.out.println("\n=== Library Management System ===");

        boolean running = true;
        while (running) {
            System.out.println("\n--- Menu ---");
            System.out.println("1. Display All Books");
            System.out.println("2. Display All Members");
            System.out.println("3. Borrow a Book");
            System.out.println("4. Return a Book");
            System.out.println("5. Add a Book");
            System.out.println("6. Exit");
            System.out.print("Choose an option: ");

            int choice = scanner.nextInt();
            scanner.nextLine();

            switch (choice) {
                case 1:
                    library.displayAllBooks();
                    break;
                case 2:
                    library.displayAllMembers();
                    break;
                case 3:
                    System.out.print("Enter Member ID: ");
                    String borrowMemberId = scanner.nextLine();
                    System.out.print("Enter Book ISBN: ");
                    String borrowIsbn = scanner.nextLine();
                    library.borrowBook(borrowMemberId, borrowIsbn);
                    break;
                case 4:
                    System.out.print("Enter Member ID: ");
                    String returnMemberId = scanner.nextLine();
                    System.out.print("Enter Book ISBN: ");
                    String returnIsbn = scanner.nextLine();
                    library.returnBook(returnMemberId, returnIsbn);
                    break;
                case 5:
                    System.out.print("Enter title: ");
                    String title = scanner.nextLine();
                    System.out.print("Enter author: ");
                    String author = scanner.nextLine();
                    System.out.print("Enter ISBN: ");
                    String isbn = scanner.nextLine();
                    System.out.print("Type (1 for Fiction, 2 for Non-Fiction): ");
                    int type = scanner.nextInt();
                    scanner.nextLine();
                    if (type == 1) {
                        System.out.print("Enter genre: ");
                        String genre = scanner.nextLine();
                        library.addBook(new FictionBook(title, author, isbn, genre));
                    } else {
                        System.out.print("Enter subject: ");
                        String subject = scanner.nextLine();
                        library.addBook(new NonFictionBook(title, author, isbn, subject));
                    }
                    break;
                case 6:
                    running = false;
                    System.out.println("Goodbye!");
                    break;
                default:
                    System.out.println("Invalid option. Please try again.");
            }
        }

        scanner.close();
    }
}

Your Library Management System is fully interactive. Users can browse books, manage memberships, and borrow or return books through a clean console menu. The Secret Mission will extend this even further with search functionality.

Secret mission

Add Search Functionality

Your library system works, but users have to scroll through the entire book list to find what they want. Can you add a search feature that finds books by title keyword or author name, handling partial matches and multiple results?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files or delete them entirely.

Resources you used:

  • The LibrarySystem project folder (contains all your Java source files and IntelliJ project configuration).

Keep everything running

No action needed. Choose this if you plan to keep building on this project or want to revisit the code later.

  • Your LibrarySystem project stays in IntelliJ IDEA and you can reopen it anytime from the Welcome screen.
  • All your Java files remain on disk in the project folder.

Pause - I'll come back to this later

There are no running processes to stop. This is a console application that only runs when you execute it manually.

  • Close the project in IntelliJ IDEA by going to File then Close Project.
  • Your files remain on disk. You can reopen the project anytime from IntelliJ's Welcome screen.

Delete - I don't want to use this again

Remove all project files and the IntelliJ project configuration from your machine.

  • In IntelliJ IDEA, go to File then Close Project to close the project.
  • On the Welcome screen, right-click LibrarySystem in the recent projects list and select Remove from Recent Projects.
  • Delete the project folder from your file system by running the appropriate command for your OS:

macOS

Open Terminal and run:

rm -rf ~/IdeaProjects/LibrarySystem

Windows

Open PowerShell and run:

Remove-Item -Recurse -Force ~\IdeaProjects\LibrarySystem

Where is my project folder?

By default, IntelliJ IDEA stores new projects in the IdeaProjects folder inside your home directory. If you chose a different location when creating the project, navigate there instead.

Nice Work!

Nice Work!

You just built a fully functional Library Management System in Java. From abstract classes to business rule enforcement, you modeled a real-world domain using clean object-oriented design.

You've learned how to:

  • Build an abstract class hierarchy with FictionBook and NonFictionBook subclasses, demonstrating inheritance and polymorphism through a shared Book reference type.
  • Create a Library class that encapsulates business logic using ArrayList and HashMap for efficient data management.
  • Implement a menu-driven console interface with real validation rules including availability checks, a 3-book borrow limit, and duplicate prevention.
  • Secret Mission: Added search-by-title and search-by-author functionality with case-insensitive partial string matching.

Ready to quiz yourself?