Java Library Management System

In the last lesson you built a Java student management system. Now let’s take the same ideas further and build a Library Management System, where each book can be on the shelf or out with a reader. We will build it slowly, one method at a time.

🎯 What we are building

This is a CRUD app with one new idea: state. Each book carries a flag that says whether it is currently issued.

What the menu lets the user do:

  • Add a new book (id, title, author). That is Create.
  • View all books and search by title or id. That is Read.
  • Issue or return a book. That is Update, because it changes the book’s state.
  • Remove a book. That is Delete.
  • Exit.

Three building blocks make it work:

  • A Book class holds the data for one book.
  • An ArrayList holds many books and grows or shrinks as needed.
  • A do-while menu loop keeps showing the options until the user quits.

Let’s start with the data.

🧱 Step 1: design the Book class

A class bundles one book’s data into one object. The new piece here is a flag that remembers whether the book is issued. Here is the Book class.

class Book {
private int id; // unique number for each book
private String title;
private String author;
private boolean isIssued; // true means the book is out with a reader
public Book(int id, String title, String author) {
this.id = id;
this.title = title;
this.author = author;
this.isIssued = false; // ✅ a new book starts on the shelf
}
public int getId() { return id; }
public String getTitle() { return title; }
public String getAuthor() { return author; }
public boolean isIssued() { return isIssued; }
public void setIssued(boolean issued) { // only the issued state can change
this.isIssued = issued;
}
@Override
public String toString() {
String status = isIssued ? "Issued" : "Available";
return "ID " + id + " | " + title + " by " + author + " | " + status;
}
}

Reading it from the top:

  • The four fields are private. That is encapsulation: outside code must go through our methods, not reach in directly.
  • The constructor takes id, title, and author. It does not take isIssued. We set it to false, because a new book always starts on the shelf.
  • Getters let other code read the values safely. For a boolean the common name is isIssued(), not getIssued().
  • Only setIssued exists. There is no setter for id, title, or author. Those should not change once a book is created.
  • toString turns the flag into words: true prints as “Issued”, false as “Available”. The user never sees a raw boolean.

The id matters more than it looks. Two books can share a title, but an id is unique. So it is the safe way to point at exactly one record when issuing, returning, and removing.

📦 Step 2: store books in an ArrayList

The book count keeps changing, so a fixed-size array is a poor fit. An ArrayList resizes itself. This line creates the list that holds every book.

import java.util.ArrayList;
ArrayList<Book> books = new ArrayList<>();

Things to notice:

  • ArrayList<Book> means a list of Book objects only. The compiler blocks the wrong type.
  • It starts empty and resizes on its own. We never set a size.
  • We keep it in main and pass it to each method, so every part works on the same shared list.

🍽️ Step 3: the menu loop

A do-while loop fits the menu, because it always shows at least once before checking whether to stop. Here is the skeleton of main with the loop and a switch. The operations are method calls for now.

import java.util.ArrayList;
import java.util.Scanner;
public class LibraryManagementSystem {
public static void main(String[] args) {
ArrayList<Book> books = new ArrayList<>();
Scanner scanner = new Scanner(System.in);
int choice;
do {
printMenu();
choice = readInt(scanner, "Choose an option: ");
switch (choice) {
case 1: addBook(books, scanner); break;
case 2: viewAll(books); break;
case 3: searchByTitle(books, scanner); break;
case 4: searchById(books, scanner); break;
case 5: issueBook(books, scanner); break;
case 6: returnBook(books, scanner); break;
case 7: removeBook(books, scanner); break;
case 8: System.out.println("Goodbye!"); break;
default: System.out.println("Invalid option. Pick 1 to 8.");
}
} while (choice != 8);
scanner.close();
}
}

Each time around the loop:

  • printMenu() shows the options.
  • readInt reads the choice as a number. We write this helper below to keep input handling in one place.
  • The switch calls the matching method, passing the books list and the scanner.
  • The loop repeats until choice is 8, then closes the scanner.

main says what happens, not how. Each “how” lives in its own method. That is the habit to keep: one method, one job.

Here is printMenu, just a block of prints.

static void printMenu() {
System.out.println("\n--- Library Management ---");
System.out.println("1. Add book");
System.out.println("2. View all books");
System.out.println("3. Search by title");
System.out.println("4. Search by id");
System.out.println("5. Issue a book");
System.out.println("6. Return a book");
System.out.println("7. Remove book");
System.out.println("8. Exit");
}

Now we build the operations one at a time.

➕ Step 4a: add a book

Read an id, title, and author, then create a Book and add it to the list.

static void addBook(ArrayList<Book> books, Scanner scanner) {
int id = readInt(scanner, "Enter id: ");
// ❌ Guard against a duplicate id before adding
if (findById(books, id) != null) {
System.out.println("An id like that already exists. Try another.");
return;
}
System.out.print("Enter title: ");
String title = scanner.nextLine();
System.out.print("Enter author: ");
String author = scanner.nextLine();
books.add(new Book(id, title, author)); // ✅ create the object, store it
System.out.println("Book added.");
}

How each part works:

  • Read the id first and check it is not taken. Duplicate ids would break search, issue, and remove later, so we stop that early.
  • findById (written below) returns the matching book or null. null means “no match.”
  • nextLine reads a whole line including spaces. A title like “Clean Code” has a space, so nextLine is right.
  • books.add(new Book(...)) builds and stores the object in one line. The constructor sets the issued flag to false.

📋 Step 4b: view all books

Loop over the list and print each book. Handle the empty list too, so the user sees a message instead of blank space.

static void viewAll(ArrayList<Book> books) {
if (books.isEmpty()) {
System.out.println("No books yet.");
return;
}
System.out.println("All books:");
for (Book b : books) {
System.out.println("- " + b); // ✅ toString runs automatically
}
}

Points to note:

  • books.isEmpty() catches the empty list. Print a message and return early.
  • The for-each loop visits each Book in turn.
  • Printing b calls our toString, so each line shows id, title, author, and Available or Issued. No need to dig out the fields by hand.

🔎 Step 4c: search by title and by id

Search by id finds at most one match, because ids are unique. Search by title might find several, because titles can repeat. First, a shared helper that finds a book by id. Many methods reuse it.

static Book findById(ArrayList<Book> books, int id) {
for (Book b : books) {
if (b.getId() == id) {
return b; // ✅ found it, hand it back
}
}
return null; // ❌ no match, signal "nothing found"
}

This helper returns the first book whose id matches, or null if none match. null is the common Java way to say “found nothing.” The caller checks for it.

Now search by title. Titles can repeat, so we check every book and print each match.

static void searchByTitle(ArrayList<Book> books, Scanner scanner) {
System.out.print("Enter title to find: ");
String target = scanner.nextLine();
boolean found = false;
for (Book b : books) {
// ✅ equalsIgnoreCase compares text and ignores capitals
if (b.getTitle().equalsIgnoreCase(target)) {
System.out.println("Found: " + b);
found = true;
}
}
if (!found) {
System.out.println("No book titled " + target + ".");
}
}

The key ideas:

  • Compare titles with equalsIgnoreCase, not ==. == checks whether two variables point to the same object. equalsIgnoreCase checks the text and treats “clean code” and “Clean Code” as a match.
  • A found flag starts false and flips to true on any match. Still false after the loop means nothing matched.

Search by id is shorter, because it leans on the helper.

static void searchById(ArrayList<Book> books, Scanner scanner) {
int id = readInt(scanner, "Enter id to find: ");
Book found = findById(books, id);
if (found == null) {
System.out.println("No book with id " + id + ".");
} else {
System.out.println("Found: " + found);
}
}

What it does:

  • Reads an id from the user.
  • Calls findById, which gives back a Book or null.
  • null means nothing found, so say so. Otherwise print the match.

📤 Step 4d: issue a book

Find the book by id, then flip its issued flag to true. The careful part is the check: lending a book that is already out makes no sense, so block it.

static void issueBook(ArrayList<Book> books, Scanner scanner) {
int id = readInt(scanner, "Enter id to issue: ");
Book b = findById(books, id);
if (b == null) {
System.out.println("No book with id " + id + ".");
return;
}
if (b.isIssued()) { // ❌ already out with someone
System.out.println("That book is already issued.");
return;
}
b.setIssued(true); // ✅ mark it as out
System.out.println("Issued: " + b);
}

Walking through it:

  • Read the id and reuse findById. No need to write the loop again.
  • No book found means say so and return early.
  • b.isIssued() already true means the book is out with someone, so refuse and return. This guard is the heart of the operation.
  • Otherwise call setIssued(true). b points to the real object in the list, so the list updates too. There is only one object, and we hold a handle to it.

📥 Step 4e: return a book

Returning is the mirror of issuing. Find the book, check it really was issued, then flip the flag back to false. Returning a book that was never out is a mistake, so we catch that too.

static void returnBook(ArrayList<Book> books, Scanner scanner) {
int id = readInt(scanner, "Enter id to return: ");
Book b = findById(books, id);
if (b == null) {
System.out.println("No book with id " + id + ".");
return;
}
if (!b.isIssued()) { // ❌ it was never out
System.out.println("That book was not issued.");
return;
}
b.setIssued(false); // ✅ back on the shelf
System.out.println("Returned: " + b);
}

The important parts:

  • Same start as issuing: find the book, return early if missing.
  • The guard is reversed. !b.isIssued() means nothing to return, so say so and stop.
  • Otherwise set the flag to false. The book is available again, and toString shows that next print.

🗑️ Step 4f: remove a book

Delete the book whose id matches. Check whether anything was actually removed, so the feedback is honest.

static void removeBook(ArrayList<Book> books, Scanner scanner) {
int id = readInt(scanner, "Enter id to remove: ");
// removeIf deletes every element that matches the test
boolean removed = books.removeIf(b -> b.getId() == id);
if (removed) {
System.out.println("Book removed.");
} else {
System.out.println("No book with id " + id + ".");
}
}

The important parts:

  • removeIf removes any element where the test is true. Here the test is “this book’s id equals the typed id.”
  • removeIf returns true if it removed anything, false if not. We use that to report the real result instead of always claiming success.

⌨️ Step 5: input handling and the Scanner pitfall

The Scanner has one trap that catches almost everyone:

  • nextInt() reads the number but leaves the Enter key (a newline) sitting in the input.
  • The next nextLine() reads that leftover newline and returns an empty string at once.
  • So it skips the user’s real answer. It looks like the program ignored a question.

We put the fix in one helper that reads a number cleanly.

static int readInt(Scanner scanner, String prompt) {
System.out.print(prompt);
while (!scanner.hasNextInt()) { // ❌ guard against non-number input
scanner.next(); // throw away the bad token
System.out.print("Please enter a whole number. " + prompt);
}
int value = scanner.nextInt();
scanner.nextLine(); // ✅ clear the leftover newline
return value;
}

Why this helper is worth it:

  • The scanner.nextLine() after the number clears the leftover newline. It lives in the helper, so we never forget it.
  • The while (!scanner.hasNextInt()) loop checks the input is a number first. Type “abc” and it throws it away and asks again, instead of crashing.
  • Every place that needs a number calls readInt, so input handling stays consistent.

The classic Scanner gotcha

After nextInt(), a newline stays in the input. The next nextLine() reads it as an empty string and seems to skip a question. The fix is a single scanner.nextLine() right after reading the number. Keeping that fix inside a helper method means you write it once and never miss it.

🧩 The finished structure

Your file has this shape. The Book class sits beside LibraryManagementSystem, and main calls the operation methods.

LibraryManagementSystem.java
├── class Book
│ ├── fields: id, title, author, isIssued
│ ├── constructor
│ ├── getId / getTitle / getAuthor / isIssued
│ ├── setIssued
│ └── toString
└── class LibraryManagementSystem
├── main (the do-while menu loop + switch)
├── printMenu
├── addBook
├── viewAll
├── searchByTitle
├── searchById
├── issueBook
├── returnBook
├── removeBook
├── findById (shared helper)
└── readInt (input helper)

Each method does one job, main ties them together, and Book holds the data. That separation makes the program easy to read and grow.

📄 Step 6: the full program

Here is everything in one file. Paste it into LibraryManagementSystem.java, compile, and run. You will recognise every piece we built above.

import java.util.ArrayList;
import java.util.Scanner;
class Book {
private int id;
private String title;
private String author;
private boolean isIssued;
public Book(int id, String title, String author) {
this.id = id;
this.title = title;
this.author = author;
this.isIssued = false;
}
public int getId() { return id; }
public String getTitle() { return title; }
public String getAuthor() { return author; }
public boolean isIssued() { return isIssued; }
public void setIssued(boolean issued) {
this.isIssued = issued;
}
@Override
public String toString() {
String status = isIssued ? "Issued" : "Available";
return "ID " + id + " | " + title + " by " + author + " | " + status;
}
}
public class LibraryManagementSystem {
public static void main(String[] args) {
ArrayList<Book> books = new ArrayList<>();
Scanner scanner = new Scanner(System.in);
int choice;
do {
printMenu();
choice = readInt(scanner, "Choose an option: ");
switch (choice) {
case 1: addBook(books, scanner); break;
case 2: viewAll(books); break;
case 3: searchByTitle(books, scanner); break;
case 4: searchById(books, scanner); break;
case 5: issueBook(books, scanner); break;
case 6: returnBook(books, scanner); break;
case 7: removeBook(books, scanner); break;
case 8: System.out.println("Goodbye!"); break;
default: System.out.println("Invalid option. Pick 1 to 8.");
}
} while (choice != 8);
scanner.close();
}
static void printMenu() {
System.out.println("\n--- Library Management ---");
System.out.println("1. Add book");
System.out.println("2. View all books");
System.out.println("3. Search by title");
System.out.println("4. Search by id");
System.out.println("5. Issue a book");
System.out.println("6. Return a book");
System.out.println("7. Remove book");
System.out.println("8. Exit");
}
static void addBook(ArrayList<Book> books, Scanner scanner) {
int id = readInt(scanner, "Enter id: ");
if (findById(books, id) != null) {
System.out.println("An id like that already exists. Try another.");
return;
}
System.out.print("Enter title: ");
String title = scanner.nextLine();
System.out.print("Enter author: ");
String author = scanner.nextLine();
books.add(new Book(id, title, author));
System.out.println("Book added.");
}
static void viewAll(ArrayList<Book> books) {
if (books.isEmpty()) {
System.out.println("No books yet.");
return;
}
System.out.println("All books:");
for (Book b : books) {
System.out.println("- " + b);
}
}
static void searchByTitle(ArrayList<Book> books, Scanner scanner) {
System.out.print("Enter title to find: ");
String target = scanner.nextLine();
boolean found = false;
for (Book b : books) {
if (b.getTitle().equalsIgnoreCase(target)) {
System.out.println("Found: " + b);
found = true;
}
}
if (!found) {
System.out.println("No book titled " + target + ".");
}
}
static void searchById(ArrayList<Book> books, Scanner scanner) {
int id = readInt(scanner, "Enter id to find: ");
Book found = findById(books, id);
if (found == null) {
System.out.println("No book with id " + id + ".");
} else {
System.out.println("Found: " + found);
}
}
static void issueBook(ArrayList<Book> books, Scanner scanner) {
int id = readInt(scanner, "Enter id to issue: ");
Book b = findById(books, id);
if (b == null) {
System.out.println("No book with id " + id + ".");
return;
}
if (b.isIssued()) {
System.out.println("That book is already issued.");
return;
}
b.setIssued(true);
System.out.println("Issued: " + b);
}
static void returnBook(ArrayList<Book> books, Scanner scanner) {
int id = readInt(scanner, "Enter id to return: ");
Book b = findById(books, id);
if (b == null) {
System.out.println("No book with id " + id + ".");
return;
}
if (!b.isIssued()) {
System.out.println("That book was not issued.");
return;
}
b.setIssued(false);
System.out.println("Returned: " + b);
}
static void removeBook(ArrayList<Book> books, Scanner scanner) {
int id = readInt(scanner, "Enter id to remove: ");
boolean removed = books.removeIf(b -> b.getId() == id);
if (removed) {
System.out.println("Book removed.");
} else {
System.out.println("No book with id " + id + ".");
}
}
static Book findById(ArrayList<Book> books, int id) {
for (Book b : books) {
if (b.getId() == id) {
return b;
}
}
return null;
}
static int readInt(Scanner scanner, String prompt) {
System.out.print(prompt);
while (!scanner.hasNextInt()) {
scanner.next();
System.out.print("Please enter a whole number. " + prompt);
}
int value = scanner.nextInt();
scanner.nextLine();
return value;
}
}

▶️ Step 7: a full sample run

Let’s run it end to end: add two books, view them, search, issue one, try to issue it again, return it, remove one, and exit.

Output

--- Library Management ---
1. Add book
2. View all books
3. Search by title
4. Search by id
5. Issue a book
6. Return a book
7. Remove book
8. Exit
Choose an option: 1
Enter id: 101
Enter title: Clean Code
Enter author: Robert Martin
Book added.
--- Library Management ---
Choose an option: 1
Enter id: 102
Enter title: The Pragmatic Programmer
Enter author: Andrew Hunt
Book added.
--- Library Management ---
Choose an option: 2
All books:
- ID 101 | Clean Code by Robert Martin | Available
- ID 102 | The Pragmatic Programmer by Andrew Hunt | Available
--- Library Management ---
Choose an option: 5
Enter id to issue: 101
Issued: ID 101 | Clean Code by Robert Martin | Issued
--- Library Management ---
Choose an option: 5
Enter id to issue: 101
That book is already issued.
--- Library Management ---
Choose an option: 6
Enter id to return: 101
Returned: ID 101 | Clean Code by Robert Martin | Available
--- Library Management ---
Choose an option: 7
Enter id to remove: 102
Book removed.
--- Library Management ---
Choose an option: 2
All books:
- ID 101 | Clean Code by Robert Martin | Available
--- Library Management ---
Choose an option: 8
Goodbye!

Each option maps to one method, and the same books list carries the data between choices. The second issue attempt is the key moment: the book is already out, so the guard stops it cleanly instead of pretending it worked. That is state-aware CRUD running live.

🛠️ Practice Challenge

Try these before checking the answers. Each builds on a method you already have.

Challenge 1: Track who borrowed each book. Add a borrower field to Book, ask for a name when issuing, clear it when returning, and show it in toString when the book is out.

Challenge 2: We already block issuing a book that is out, but make sure you understand why. Add a menu option that lists only the issued books, so the librarian can see what is currently lent out.

⚠️ Common Mistakes

A few slip-ups to watch for.

  • Forgetting to clear the Scanner newline. After nextInt(), add scanner.nextLine() before reading a String, or the text input gets skipped. Putting it in a helper solves it once.
  • Issuing a book that is already out. Always check isIssued() before lending. Without the guard you lose track of the real state and the real borrower.
  • Returning a book that was never issued. Check !isIssued() before returning, so you do not flip the flag on a book that was already on the shelf.
  • Comparing titles with ==. Use .equals() or .equalsIgnoreCase() to compare the text. == checks whether two variables point to the same object, which is rarely what you mean for text.
  • Letting duplicate ids in. If two books share an id, search, issue, and remove get confused about which one you mean. Check for a duplicate id before adding.
  • Assuming a remove always worked. removeIf might match nothing. Check its true/false result and tell the user honestly.

✅ Best Practices

Habits that make console projects easy to read and grow.

  • Use a class for your data. A Book class keeps each record’s fields together, including its issued state, and protects them with encapsulation.
  • Use an ArrayList for the collection. It resizes as books are added and removed, so you never manage a fixed size.
  • Give each operation its own method. addBook, viewAll, issueBook, and the rest each do one job. main stays short and readable.
  • Share helpers, do not repeat code. findById and readInt are written once and reused everywhere.
  • Guard state changes. Check the issued flag before issuing or returning, so the program never lies about where a book is.

🧩 What You’ve Learned

Nicely done. You built a real, complete app that tracks state, not just records. Let’s recap.

  • ✅ A Library Management System is a CRUD app where each record also carries state: a book is Available or Issued.
  • ✅ A Book class holds each record’s data (id, title, author, isIssued) using encapsulation, a constructor, getters, a setter, and toString.
  • ✅ An ArrayList stores the books and resizes as they are added or removed.
  • ✅ A do-while menu loop with a switch drives the whole program, calling one method per option.
  • ✅ A shared findById helper powers search, issue, return, and remove without repeating the loop.
  • Guards keep the data honest: block issuing a book that is already out, and block returning one that was never issued.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does the isIssued flag on a Book represent?

    Why: isIssued is a boolean that is true when the book is lent out and false when it is on the shelf.

  2. 2

    Why check isIssued() before issuing a book?

    Why: The guard prevents issuing a book that is already issued, which would lose track of its real state.

  3. 3

    Why call scanner.nextLine() after nextInt()?

    Why: nextInt() leaves a newline that would make the next nextLine() read an empty string.

  4. 4

    How should you compare a searched title to a book's title?

    Why: Use equals/equalsIgnoreCase to compare the text; == compares references.

🚀 What’s Next?

Next we build a project where state and rules matter even more: money moving between accounts, with balances that must never go wrong. Let’s build it.

Java Banking Application

Share & Connect