LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
JavaIntermediate~2.5 hours

Library Management System

Track books, members, and due dates using inheritance for different book/member types.

InheritancePolymorphismFile Handling

Overview

Every library has more than one kind of member, and every kind follows different rules — a student can usually borrow a handful of books at a time, while a faculty member is typically allowed far more. Modeling that in Java is a textbook use case for inheritance and polymorphism: StudentMember and FacultyMember are both unmistakably a Member, sharing an id, a name, and a list of borrowed titles, but each supplies its own answer to "how many books can you borrow at once?" That is exactly what an abstract method is for.

By the end of this tutorial you will have a console library application with an abstract Member base class, two concrete subclasses (StudentMember and FacultyMember), a Book class, and a Library class that manages both collections and persists the book catalog to disk with java.io. The most important moment in the whole project is a single line — member.canBorrow() — that behaves differently depending on whether member actually refers to a StudentMember or a FacultyMember object, decided automatically at runtime. That is polymorphism working exactly as intended.

What You'll Build
  • A Book class tracking title, author, ISBN, and issued/available status.
  • An abstract Member base class with an abstract getBorrowLimit() method and a shared canBorrow().
  • StudentMember and FacultyMember subclasses that each override getBorrowLimit() with their own privilege.
  • A Library class holding an ArrayList<Book> and an ArrayList<Member> together.
  • Polymorphic issueBook()/returnBook() operations that respect each member's actual borrow limit.
  • File persistence for the book catalog using java.io.FileWriter and BufferedReader.

Prerequisites

  • Classes — private/protected/public access and member methods.
  • Inheritance — deriving a class with class Sub extends Base and calling super(...).
  • Polymorphism — abstract classes, abstract methods, and the @Override annotation.
  • Collections basics — ArrayList<Book> and ArrayList<Member>.
  • File handling in Java — writing with FileWriter/BufferedWriter and reading with BufferedReader, plus basic try/catch around IOException.

Project Structure

The program lives in a single file, LibraryManagementSystem.java. Book is a plain, non-polymorphic class — every book behaves the same way regardless of what it contains. Member, by contrast, is deliberately declared abstract: it defines an abstract getBorrowLimit() method with no body, which means Member itself can never be instantiated with new Member(...) — only StudentMember and FacultyMember, which each supply a real implementation, can. Library stores books and members together in an ArrayList<Book> and an ArrayList<Member>; the member list can hold either subclass interchangeably because both are-a Member.

The catalog is persisted as plain comma-separated text, one line per book, using java.io.FileWriter to write and java.io.BufferedReader to read it back with readLine(). That is the same reason a C++ or Python program would choose text over raw binary here: comma-separated text is trivial to inspect, edit by hand, and parse back with String.split(","), and it does not depend on any particular class layout staying stable between runs.

Step 1: Define the Book Class

A Book is simple enough to fully define in one step: an ISBN, title, author, and two fields tracking whether it is currently issued and to whom. markIssued()/markReturned() are the only ways those two fields ever change, keeping a book's issued status consistent no matter which part of the program touches it.

class Book {
private String isbn; // Unique identifier for this book
private String title; // Book title
private String author; // Book author
private boolean issued; // Whether this book is currently checked out
private int issuedToId = -1; // Member id currently holding this book; -1 means not issued
public Book(String isbn, String title, String author) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.issued = false; // New books always start available
}
public String getIsbn() {
return isbn;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public boolean isIssued() {
return issued;
}
public int getIssuedToId() {
return issuedToId;
}
public void markIssued(int memberId) { // Called by the Library when a book is checked out
issued = true;
issuedToId = memberId;
}
public void markReturned() { // Called by the Library when a book comes back
issued = false;
issuedToId = -1;
}
@Override
public String toString() {
return isbn + " | " + title + " by " + author + (issued ? " [ISSUED]" : " [AVAILABLE]");
}
}

Step 2: Define an Abstract Member Base Class

Every member — regardless of type — has an id, a name, and a list of currently borrowed titles, so those live in Member as protected (not private) fields, meaning StudentMember and FacultyMember can read them directly in their own methods instead of going through an accessor. getBorrowLimit() is declared abstract, which means Member provides no body for it at all, and as a direct consequence Java requires the entire class to be marked abstract too — the compiler will refuse to compile new Member(...) anywhere in the program.

import java.util.ArrayList;
import java.util.List;
abstract class Member {
protected int id; // protected, not private, so subclasses can read it directly
protected String name; // Member's display name
protected List<String> borrowedBooks = new ArrayList<>(); // Titles this member currently has checked out
public Member(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public abstract int getBorrowLimit(); // No body here: every concrete subclass must supply its own limit
public abstract String getRole(); // No body here: identifies the concrete type for display purposes
}

An abstract method is not "a method that does nothing" — it is "no default exists, and every concrete subclass must provide one." That is different from an ordinary (non-abstract) method defined in Member, which does have a body and which a subclass may optionally override.

Step 3: Implement Member's Shared Behavior

canBorrow() is where polymorphism does real work: it calls getBorrowLimit() on this, and because that method is abstract in Member but overridden in each subclass, the call resolves at runtime to whichever concrete class the object actually is. Three books borrowed is under the limit for a FacultyMember but over the limit for a StudentMember, and canBorrow() does not need an if/else to know which one it is talking to — Java's dynamic dispatch handles that automatically.

public boolean canBorrow() {
return borrowedBooks.size() < getBorrowLimit(); // Dispatches to whichever subclass's real getBorrowLimit() applies
}
public void borrowBook(String title) {
borrowedBooks.add(title); // Record that this member now holds this title
}
public void returnBook(String title) {
borrowedBooks.remove(title); // ArrayList.remove(Object) removes the first matching element, if any
}
public void displayInfo() {
System.out.println("ID: " + id + " | Name: " + name + " | Role: " + getRole()
+ " | Borrow Limit: " + getBorrowLimit() + " | Currently Borrowed: " + borrowedBooks.size());
}

displayInfo() is never overridden by StudentMember or FacultyMember in this project — it does not need to be, since it already produces the correct output for either subclass by calling the abstract getRole() and getBorrowLimit() internally. This is a subtle but important point: a subclass benefits from polymorphism the moment it overrides even one small method that a larger, un-overridden method depends on.

Step 4: Derive StudentMember and FacultyMember

With Member doing all the shared work, each subclass is remarkably small: a constructor that forwards to Member's constructor with super(id, name), and two one-line overrides supplying that type's actual borrow limit and role label. @Override is not strictly required by the compiler, but it is good practice — it tells the compiler "I intend to override a method from the base class," and the compiler produces an error if the signature does not actually match one, catching typos immediately instead of silently creating an unrelated new method.

class StudentMember extends Member { // "extends Member" means a StudentMember IS-A Member everywhere one is expected
public StudentMember(int id, String name) {
super(id, name); // Forwards straight to Member's constructor
}
@Override
public int getBorrowLimit() {
return 3; // Students may have at most 3 books out at once
}
@Override
public String getRole() {
return "Student";
}
}
class FacultyMember extends Member {
public FacultyMember(int id, String name) {
super(id, name);
}
@Override
public int getBorrowLimit() {
return 10; // Faculty get a much higher borrowing privilege
}
@Override
public String getRole() {
return "Faculty";
}
}

Step 5: Build the Library Class

Library owns both collections: books in an ArrayList<Book>, since a book never needs to "become" a different kind of book, and members in an ArrayList<Member>, since a Member reference can point at either a StudentMember or a FacultyMember interchangeably. Java's garbage collector reclaims member objects automatically once nothing references them anymore, so unlike a manually memory-managed language, Library does not need a destructor to free them.

class Library {
private List<Book> books = new ArrayList<>(); // Every book in the catalog
private List<Member> members = new ArrayList<>(); // Base-type list so Students and Faculty are stored together
public void addBook(String isbn, String title, String author) {
books.add(new Book(isbn, title, author));
}
public void addMember(Member m) {
members.add(m); // Accepts a StudentMember or FacultyMember interchangeably, since both extend Member
}
public Book findBook(String isbn) {
for (Book b : books) {
if (b.getIsbn().equals(isbn)) { // Use .equals() for String comparison, never == for content equality
return b;
}
}
return null;
}
public Member findMember(int id) {
for (Member m : members) {
if (m.getId() == id) {
return m;
}
}
return null;
}
public void listBooks() {
System.out.println("\n--- Library Catalog ---");
for (Book b : books) {
System.out.println(b); // Uses Book's toString() from Step 1
}
}
public void listMembers() {
System.out.println("\n--- Library Members ---");
for (Member m : members) {
m.displayInfo(); // Polymorphic call: runs each member's real (Student or Faculty) data through Member
}
}
public List<Book> getBooks() { // Exposed for Step 7's file persistence
return books;
}
}

Step 6: Issue and Return Books Polymorphically

issueBook() is where every piece built so far comes together: it looks up the book and member, rejects the request if the book is already out or the member does not exist, and then calls member.canBorrow() — the same polymorphic check from Step 3 — to enforce whichever borrow limit actually applies to that member.

public void issueBook(String isbn, int memberId) {
Book book = findBook(isbn);
Member member = findMember(memberId);
if (book == null) {
System.out.println("Book not found.");
return;
}
if (member == null) {
System.out.println("Member not found.");
return;
}
if (book.isIssued()) {
System.out.println("That book is already issued.");
return;
}
if (!member.canBorrow()) { // canBorrow() uses the member's real (subclass) borrow limit via dynamic dispatch
System.out.println(member.getName() + " has reached their borrow limit of " + member.getBorrowLimit() + ".");
return;
}
book.markIssued(memberId);
member.borrowBook(book.getTitle());
System.out.println("Issued \"" + book.getTitle() + "\" to " + member.getName() + ".");
}
public void returnBook(String isbn) {
Book book = findBook(isbn);
if (book == null || !book.isIssued()) {
System.out.println("That book is not currently issued.");
return;
}
Member member = findMember(book.getIssuedToId()); // Look up whoever currently holds the book
if (member != null) {
member.returnBook(book.getTitle()); // Remove the title from that member's borrowed list
}
book.markReturned();
System.out.println("\"" + book.getTitle() + "\" has been returned.");
}

Step 7: Persist the Catalog to a File

saveToFile() writes one comma-separated line per book with FileWriter, and loadFromFile() reads those lines back with BufferedReader.readLine(), splitting each line on commas with String.split(",") to recover the ISBN, title, and author. Both methods are wrapped in try/catch (IOException e), since IOException is itself a checked exception — the compiler requires file operations to be handled the same way withdraw() required handling InsufficientFundsException in the previous project.

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public void saveToFile(String filename) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) { // try-with-resources closes automatically
for (Book b : books) {
// Comma-separated line: isbn,title,author
writer.write(b.getIsbn() + "," + b.getTitle() + "," + b.getAuthor());
writer.newLine();
}
} catch (IOException e) {
System.out.println("Could not save catalog: " + e.getMessage());
}
}
public void loadFromFile(String filename) {
File file = new File(filename);
if (!file.exists()) { // No file yet means this is the first run; nothing to load
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
while ((line = reader.readLine()) != null) { // Read one comma-separated record per line until EOF
String[] parts = line.split(","); // Splits "isbn,title,author" into 3 fields
if (parts.length == 3) {
addBook(parts[0], parts[1], parts[2]);
}
}
} catch (IOException e) {
System.out.println("Could not load catalog: " + e.getMessage());
}
}

The try (BufferedWriter writer = ...) syntax is called try-with-resources: any resource declared inside the parentheses that implements AutoCloseable (which BufferedWriter and BufferedReader both do) is automatically closed when the block exits, whether it exits normally or because of an exception — removing the need for a manual finally { writer.close(); } block.

Step 8: Build the Menu Loop

main() loads any previously saved catalog, seeds a couple of starting members, then loops over a menu that lets the user add more books/members, issue and return books, and list the catalog and roster, saving the catalog back to disk after every book is added so no addition is lost even if the program closes unexpectedly.

import java.io.File;
import java.util.Scanner;
public class LibraryManagementSystem {
public static void main(String[] args) {
Library library = new Library();
String filename = "catalog.txt";
library.loadFromFile(filename); // Restore whatever catalog was saved from a previous run
// Seed the member list with a couple of starting records; addMember() accepts either subclass polymorphically
library.addMember(new StudentMember(1, "Aditi Sharma"));
library.addMember(new FacultyMember(2, "Dr. Rakesh Gupta"));
Scanner scanner = new Scanner(System.in);
int choice;
do {
System.out.println("\n===== LIBRARY MANAGEMENT SYSTEM =====");
System.out.println("1. Add Book");
System.out.println("2. Add Member");
System.out.println("3. Issue Book");
System.out.println("4. Return Book");
System.out.println("5. List Books");
System.out.println("6. List Members");
System.out.println("7. Exit");
System.out.print("Enter your choice: ");
choice = scanner.nextInt();
scanner.nextLine(); // Discard the leftover newline left behind by nextInt()
if (choice == 1) {
System.out.print("Enter ISBN: ");
String isbn = scanner.nextLine();
System.out.print("Enter title: ");
String title = scanner.nextLine();
System.out.print("Enter author: ");
String author = scanner.nextLine();
library.addBook(isbn, title, author);
library.saveToFile(filename); // Persist immediately so a new book survives an unexpected exit
System.out.println("Book added.");
} else if (choice == 2) {
System.out.print("Enter member ID: ");
int id = scanner.nextInt();
scanner.nextLine();
System.out.print("Enter member name: ");
String name = scanner.nextLine();
System.out.print("Enter member type (student/faculty): ");
String type = scanner.nextLine();
if (type.equalsIgnoreCase("faculty")) {
library.addMember(new FacultyMember(id, name)); // Polymorphism: stored as a Member either way
} else {
library.addMember(new StudentMember(id, name));
}
System.out.println("Member added.");
} else if (choice == 3) {
System.out.print("Enter ISBN to issue: ");
String isbn = scanner.nextLine();
System.out.print("Enter member ID: ");
int memberId = scanner.nextInt();
scanner.nextLine();
library.issueBook(isbn, memberId);
} else if (choice == 4) {
System.out.print("Enter ISBN to return: ");
String isbn = scanner.nextLine();
library.returnBook(isbn);
} else if (choice == 5) {
library.listBooks();
} else if (choice == 6) {
library.listMembers();
} else if (choice == 7) {
System.out.println("Goodbye!");
} else {
System.out.println("Invalid choice, try again.");
}
} while (choice != 7);
scanner.close();
}
}

Complete Code

Here is the full program with every class assembled in the correct order, ready to save as LibraryManagementSystem.java and run with javac LibraryManagementSystem.java && java LibraryManagementSystem.

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
class Book {
private String isbn;
private String title;
private String author;
private boolean issued;
private int issuedToId = -1;
public Book(String isbn, String title, String author) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.issued = false;
}
public String getIsbn() {
return isbn;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public boolean isIssued() {
return issued;
}
public int getIssuedToId() {
return issuedToId;
}
public void markIssued(int memberId) {
issued = true;
issuedToId = memberId;
}
public void markReturned() {
issued = false;
issuedToId = -1;
}
@Override
public String toString() {
return isbn + " | " + title + " by " + author + (issued ? " [ISSUED]" : " [AVAILABLE]");
}
}
abstract class Member {
protected int id;
protected String name;
protected List<String> borrowedBooks = new ArrayList<>();
public Member(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public abstract int getBorrowLimit();
public abstract String getRole();
public boolean canBorrow() {
return borrowedBooks.size() < getBorrowLimit();
}
public void borrowBook(String title) {
borrowedBooks.add(title);
}
public void returnBook(String title) {
borrowedBooks.remove(title);
}
public void displayInfo() {
System.out.println("ID: " + id + " | Name: " + name + " | Role: " + getRole()
+ " | Borrow Limit: " + getBorrowLimit() + " | Currently Borrowed: " + borrowedBooks.size());
}
}
class StudentMember extends Member {
public StudentMember(int id, String name) {
super(id, name);
}
@Override
public int getBorrowLimit() {
return 3;
}
@Override
public String getRole() {
return "Student";
}
}
class FacultyMember extends Member {
public FacultyMember(int id, String name) {
super(id, name);
}
@Override
public int getBorrowLimit() {
return 10;
}
@Override
public String getRole() {
return "Faculty";
}
}
class Library {
private List<Book> books = new ArrayList<>();
private List<Member> members = new ArrayList<>();
public void addBook(String isbn, String title, String author) {
books.add(new Book(isbn, title, author));
}
public void addMember(Member m) {
members.add(m);
}
public Book findBook(String isbn) {
for (Book b : books) {
if (b.getIsbn().equals(isbn)) {
return b;
}
}
return null;
}
public Member findMember(int id) {
for (Member m : members) {
if (m.getId() == id) {
return m;
}
}
return null;
}
public void listBooks() {
System.out.println("\n--- Library Catalog ---");
for (Book b : books) {
System.out.println(b);
}
}
public void listMembers() {
System.out.println("\n--- Library Members ---");
for (Member m : members) {
m.displayInfo();
}
}
public List<Book> getBooks() {
return books;
}
public void issueBook(String isbn, int memberId) {
Book book = findBook(isbn);
Member member = findMember(memberId);
if (book == null) {
System.out.println("Book not found.");
return;
}
if (member == null) {
System.out.println("Member not found.");
return;
}
if (book.isIssued()) {
System.out.println("That book is already issued.");
return;
}
if (!member.canBorrow()) {
System.out.println(member.getName() + " has reached their borrow limit of " + member.getBorrowLimit() + ".");
return;
}
book.markIssued(memberId);
member.borrowBook(book.getTitle());
System.out.println("Issued \"" + book.getTitle() + "\" to " + member.getName() + ".");
}
public void returnBook(String isbn) {
Book book = findBook(isbn);
if (book == null || !book.isIssued()) {
System.out.println("That book is not currently issued.");
return;
}
Member member = findMember(book.getIssuedToId());
if (member != null) {
member.returnBook(book.getTitle());
}
book.markReturned();
System.out.println("\"" + book.getTitle() + "\" has been returned.");
}
public void saveToFile(String filename) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
for (Book b : books) {
writer.write(b.getIsbn() + "," + b.getTitle() + "," + b.getAuthor());
writer.newLine();
}
} catch (IOException e) {
System.out.println("Could not save catalog: " + e.getMessage());
}
}
public void loadFromFile(String filename) {
File file = new File(filename);
if (!file.exists()) {
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(",");
if (parts.length == 3) {
addBook(parts[0], parts[1], parts[2]);
}
}
} catch (IOException e) {
System.out.println("Could not load catalog: " + e.getMessage());
}
}
}
public class LibraryManagementSystem {
public static void main(String[] args) {
Library library = new Library();
String filename = "catalog.txt";
library.loadFromFile(filename);
library.addMember(new StudentMember(1, "Aditi Sharma"));
library.addMember(new FacultyMember(2, "Dr. Rakesh Gupta"));
Scanner scanner = new Scanner(System.in);
int choice;
do {
System.out.println("\n===== LIBRARY MANAGEMENT SYSTEM =====");
System.out.println("1. Add Book");
System.out.println("2. Add Member");
System.out.println("3. Issue Book");
System.out.println("4. Return Book");
System.out.println("5. List Books");
System.out.println("6. List Members");
System.out.println("7. Exit");
System.out.print("Enter your choice: ");
choice = scanner.nextInt();
scanner.nextLine();
if (choice == 1) {
System.out.print("Enter ISBN: ");
String isbn = scanner.nextLine();
System.out.print("Enter title: ");
String title = scanner.nextLine();
System.out.print("Enter author: ");
String author = scanner.nextLine();
library.addBook(isbn, title, author);
library.saveToFile(filename);
System.out.println("Book added.");
} else if (choice == 2) {
System.out.print("Enter member ID: ");
int id = scanner.nextInt();
scanner.nextLine();
System.out.print("Enter member name: ");
String name = scanner.nextLine();
System.out.print("Enter member type (student/faculty): ");
String type = scanner.nextLine();
if (type.equalsIgnoreCase("faculty")) {
library.addMember(new FacultyMember(id, name));
} else {
library.addMember(new StudentMember(id, name));
}
System.out.println("Member added.");
} else if (choice == 3) {
System.out.print("Enter ISBN to issue: ");
String isbn = scanner.nextLine();
System.out.print("Enter member ID: ");
int memberId = scanner.nextInt();
scanner.nextLine();
library.issueBook(isbn, memberId);
} else if (choice == 4) {
System.out.print("Enter ISBN to return: ");
String isbn = scanner.nextLine();
library.returnBook(isbn);
} else if (choice == 5) {
library.listBooks();
} else if (choice == 6) {
library.listMembers();
} else if (choice == 7) {
System.out.println("Goodbye!");
} else {
System.out.println("Invalid choice, try again.");
}
} while (choice != 7);
scanner.close();
}
}

Sample Run

Sample Run

Click Run to see what this code prints.

Extend This Project

  • Add a LibrarianMember subclass with an even higher (or unlimited) borrow limit and the ability to add books directly.
  • Track a due date per borrowed book and calculate a fine in a new abstract calculateFine(int daysLate) method, overridden differently per role (e.g. Faculty pay no fine).
  • Persist members (not just books) to their own file using the same comma-separated pattern from Step 7.
  • Let Library.listMembers() accept a role filter (e.g. only Students) using member instanceof StudentMember to check each member's concrete type.
  • Wrap the whole catalog file in a try/catch that also handles a corrupted line (fewer than 3 comma-separated fields) without crashing the load.

Summary

You built a library system where the borrowing rules genuinely differ by member type, enforced through an abstract getBorrowLimit() rather than an if/else chain checking a "type" flag. That difference matters: adding a third member type later means writing one new small subclass, not hunting down every place in the program that branches on member type. Real polymorphism, the abstract base class that makes it possible, and the file persistence layered on top are some of the most valuable tools Java gives you for keeping growing programs manageable.