Java Notes
Build a Library Management System with Java covering book management, member registration, issue/return tracking, and fine calculation.
Project Overview
The Library Management System is a comprehensive Java application that automates all library operations including book catalog management, member registration, book issue/return tracking, fine calculation for overdue books, reservation queue management, and report generation. This project demonstrates core Java OOP concepts when implemented as a console app, or Spring Boot REST API skills when built as a web application.
This project is ideal for:
- Java beginners learning OOP concepts (inheritance, polymorphism, encapsulation)
- Students building their first database-connected Java project
- Developers learning Spring Boot with a familiar domain
- Anyone preparing for Java interviews with a relatable project
The system manages multiple book categories (Fiction, Non-Fiction, Reference, Periodicals), supports different member types (Student, Faculty, External), and implements business rules like maximum books per member, overdue fine calculation, and reservation priority.
Features
- Book Management — Add, update, delete, and search books with ISBN validation
- Member Registration — Register students, faculty, and external members with ID cards
- Book Issue — Issue books with due date calculation based on member type
- Book Return — Return books with automatic fine calculation for overdue
- Fine Calculation — ₹2/day for students, ₹5/day for external members
- Book Reservation — Reserve books currently issued to others (queue system)
- Search Catalog — Search by title, author, ISBN, category, or keyword
- Barcode System — Generate and scan barcodes for books and member cards
- Multiple Copies — Track individual copies of the same book title
- Category Management — Organize books into categories and subcategories
- Author Management — Maintain author database with biography
- Member History — Complete borrowing history for each member
- Overdue Notifications — Email reminders for approaching and past due dates
- Reports — Monthly issue/return reports, popular books, active members
- Rack Location — Track physical location of books (floor, rack, shelf)
- Renewal — Renew issued books (max 2 renewals per issue)
- Bulk Import — Import book catalog from CSV/Excel
- Dashboard — Total books, issued count, available count, overdue count
- Access Control — Librarian (full access), Assistant (limited), Member (self-service)
- Receipt Generation — Print/PDF receipt for issue and return transactions
Project Structure
Database Schema
books Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique book title ID |
| isbn | VARCHAR(13) | UNIQUE, NOT NULL | ISBN-13 number |
| title | VARCHAR(255) | NOT NULL | Book title |
| publisher | VARCHAR(100) | Publisher name | |
| publish_year | INT | Year of publication | |
| edition | VARCHAR(20) | Book edition | |
| pages | INT | Number of pages | |
| language | VARCHAR(30) | DEFAULT 'English' | Language |
| category_id | BIGINT | FOREIGN KEY | Category reference |
| description | TEXT | Book description | |
| cover_image_url | VARCHAR(255) | Cover image | |
| total_copies | INT | DEFAULT 1 | Total copies owned |
| available_copies | INT | DEFAULT 1 | Currently available |
| created_at | TIMESTAMP | NOT NULL | Added to library date |
book_copies Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique copy ID |
| book_id | BIGINT | FOREIGN KEY | Reference to book |
| barcode | VARCHAR(20) | UNIQUE, NOT NULL | Physical barcode |
| status | ENUM | DEFAULT 'AVAILABLE' | AVAILABLE, ISSUED, RESERVED, DAMAGED, LOST |
| rack_location | VARCHAR(20) | Physical location (F2-R3-S5) | |
| condition_notes | VARCHAR(255) | Condition description | |
| acquired_date | DATE | NOT NULL | Date acquired |
members Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique member ID |
| member_id | VARCHAR(10) | UNIQUE, NOT NULL | Library card number |
| name | VARCHAR(100) | NOT NULL | Full name |
| VARCHAR(100) | UNIQUE, NOT NULL | Email address | |
| phone | VARCHAR(15) | Contact number | |
| member_type | ENUM | NOT NULL | STUDENT, FACULTY, EXTERNAL |
| max_books | INT | NOT NULL | Max books allowed |
| max_days | INT | NOT NULL | Max issue days |
| status | ENUM | DEFAULT 'ACTIVE' | ACTIVE, SUSPENDED, EXPIRED |
| membership_expiry | DATE | NOT NULL | Card expiry date |
| created_at | TIMESTAMP | NOT NULL | Registration date |
transactions Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique ID |
| book_copy_id | BIGINT | FOREIGN KEY | Issued book copy |
| member_id | BIGINT | FOREIGN KEY | Member who borrowed |
| issue_date | DATE | NOT NULL | Date of issue |
| due_date | DATE | NOT NULL | Expected return date |
| return_date | DATE | Actual return date | |
| renewal_count | INT | DEFAULT 0 | Times renewed |
| transaction_type | ENUM | NOT NULL | ISSUE, RETURN, RENEWAL |
| fine_amount | DECIMAL(8,2) | DEFAULT 0 | Fine charged |
| fine_paid | BOOLEAN | DEFAULT FALSE | Fine payment status |
| issued_by | VARCHAR(50) | Librarian who issued |
reservations Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique ID |
| book_id | BIGINT | FOREIGN KEY | Reserved book title |
| member_id | BIGINT | FOREIGN KEY | Member who reserved |
| reserved_date | TIMESTAMP | NOT NULL | Reservation time |
| queue_position | INT | NOT NULL | Position in queue |
| status | ENUM | DEFAULT 'WAITING' | WAITING, NOTIFIED, FULFILLED, CANCELLED |
| notified_at | TIMESTAMP | When notification sent | |
| expires_at | TIMESTAMP | Pickup deadline |
Entity Relationships
Key Code Snippets
Book Entity
Transaction Service (Issue & Return Logic)
package com.wohotech.library.service;
import com.wohotech.library.dto.IssueRequest;
import com.wohotech.library.dto.ReturnRequest;
import com.wohotech.library.dto.TransactionResponse;
import com.wohotech.library.entity.*;
import com.wohotech.library.enums.*;
import com.wohotech.library.exception.*;
import com.wohotech.library.repository.*;
import com.wohotech.library.util.FineCalculator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class TransactionServiceImpl implements TransactionService {
private final TransactionRepository transactionRepository;
private final BookRepository bookRepository;
private final BookCopyRepository bookCopyRepository;
private final MemberRepository memberRepository;
private final ReservationService reservationService;
private final FineCalculator fineCalculator;
private final NotificationService notificationService;
@Override
@Transactional
public TransactionResponse issueBook(IssueRequest request) {
log.info("Issuing book copy {} to member {}", request.getBarcode(), request.getMemberId());
// 1. Validate member
Member member = memberRepository.findByMemberId(request.getMemberId())
.orElseThrow(() -> new MemberNotFoundException(
"Member not found: " + request.getMemberId()));
if (member.getStatus() != MemberStatus.ACTIVE) {
throw new RuntimeException("Member account is " + member.getStatus());
}
if (member.getMembershipExpiry().isBefore(LocalDate.now())) {
throw new RuntimeException("Membership expired on " + member.getMembershipExpiry());
}
// 2. Check if member has pending fines
BigDecimal pendingFines = transactionRepository.getPendingFines(member.getId());
if (pendingFines.compareTo(BigDecimal.ZERO) > 0) {
throw new OverdueBooksException(
"Please pay pending fine of ₹" + pendingFines + " before issuing new books");
}
// 3. Check max books limit
long currentlyIssued = transactionRepository.countCurrentlyIssued(member.getId());
if (currentlyIssued >= member.getMaxBooks()) {
throw new MaxBooksExceededException(
"Maximum books limit reached (" + member.getMaxBooks() +
"). Return a book before issuing new one.");
}
// 4. Validate book copy
BookCopy bookCopy = bookCopyRepository.findByBarcode(request.getBarcode())
.orElseThrow(() -> new RuntimeException("Book copy not found: " + request.getBarcode()));
if (bookCopy.getStatus() != BookStatus.AVAILABLE) {
throw new BookNotAvailableException(
"Book copy is currently " + bookCopy.getStatus());
}
// 5. Calculate due date based on member type
LocalDate dueDate = LocalDate.now().plusDays(member.getMaxDays());
// 6. Create transaction
Transaction transaction = Transaction.builder()
.bookCopy(bookCopy)
.member(member)
.issueDate(LocalDate.now())
.dueDate(dueDate)
.transactionType(TransactionType.ISSUE)
.renewalCount(0)
.fineAmount(BigDecimal.ZERO)
.finePaid(false)
.issuedBy(request.getIssuedBy())
.build();
transactionRepository.save(transaction);
// 7. Update book copy status
bookCopy.setStatus(BookStatus.ISSUED);
bookCopyRepository.save(bookCopy);
// 8. Decrease available copies count
Book book = bookCopy.getBook();
book.decreaseAvailableCopies();
bookRepository.save(book);
log.info("Book issued successfully. Due date: {}", dueDate);
return TransactionResponse.builder()
.transactionId(transaction.getId())
.bookTitle(book.getTitle())
.barcode(bookCopy.getBarcode())
.memberName(member.getName())
.memberId(member.getMemberId())
.issueDate(LocalDate.now())
.dueDate(dueDate)
.message("Book issued successfully")
.build();
}
@Override
@Transactional
public TransactionResponse returnBook(ReturnRequest request) {
log.info("Returning book copy: {}", request.getBarcode());
// 1. Find the active transaction for this book copy
BookCopy bookCopy = bookCopyRepository.findByBarcode(request.getBarcode())
.orElseThrow(() -> new RuntimeException("Book copy not found"));
Transaction transaction = transactionRepository
.findActiveTransactionByBookCopy(bookCopy.getId())
.orElseThrow(() -> new RuntimeException("No active issue found for this book"));
// 2. Calculate fine if overdue
LocalDate returnDate = LocalDate.now();
BigDecimal fineAmount = BigDecimal.ZERO;
if (returnDate.isAfter(transaction.getDueDate())) {
long overdueDays = ChronoUnit.DAYS.between(transaction.getDueDate(), returnDate);
fineAmount = fineCalculator.calculateFine(
transaction.getMember().getMemberType(), overdueDays);
log.info("Book overdue by {} days. Fine: ₹{}", overdueDays, fineAmount);
}
// 3. Update transaction
transaction.setReturnDate(returnDate);
transaction.setFineAmount(fineAmount);
transaction.setTransactionType(TransactionType.RETURN);
transactionRepository.save(transaction);
// 4. Update book copy status
bookCopy.setStatus(BookStatus.AVAILABLE);
bookCopyRepository.save(bookCopy);
// 5. Increase available copies
Book book = bookCopy.getBook();
book.increaseAvailableCopies();
bookRepository.save(book);
// 6. Check if anyone has reserved this book
reservationService.notifyNextInQueue(book.getId());
log.info("Book returned successfully. Fine: ₹{}", fineAmount);
return TransactionResponse.builder()
.transactionId(transaction.getId())
.bookTitle(book.getTitle())
.barcode(bookCopy.getBarcode())
.memberName(transaction.getMember().getName())
.memberId(transaction.getMember().getMemberId())
.issueDate(transaction.getIssueDate())
.returnDate(returnDate)
.dueDate(transaction.getDueDate())
.fineAmount(fineAmount)
.message(fineAmount.compareTo(BigDecimal.ZERO) > 0
? "Book returned. Fine of ₹" + fineAmount + " applicable."
: "Book returned successfully. No fine.")
.build();
}
@Override
@Transactional
public TransactionResponse renewBook(String barcode, String memberId) {
BookCopy bookCopy = bookCopyRepository.findByBarcode(barcode)
.orElseThrow(() -> new RuntimeException("Book copy not found"));
Transaction transaction = transactionRepository
.findActiveTransactionByBookCopy(bookCopy.getId())
.orElseThrow(() -> new RuntimeException("No active issue found"));
// Check renewal limit (max 2)
if (transaction.getRenewalCount() >= 2) {
throw new RuntimeException("Maximum renewal limit (2) reached");
}
// Check if book is reserved by someone else
if (reservationService.hasActiveReservation(bookCopy.getBook().getId())) {
throw new RuntimeException("Cannot renew - book is reserved by another member");
}
// Check if already overdue
if (LocalDate.now().isAfter(transaction.getDueDate())) {
throw new RuntimeException("Cannot renew overdue book. Please return and pay fine.");
}
// Extend due date
Member member = transaction.getMember();
LocalDate newDueDate = transaction.getDueDate().plusDays(member.getMaxDays());
transaction.setDueDate(newDueDate);
transaction.setRenewalCount(transaction.getRenewalCount() + 1);
transactionRepository.save(transaction);
return TransactionResponse.builder()
.transactionId(transaction.getId())
.bookTitle(bookCopy.getBook().getTitle())
.dueDate(newDueDate)
.message("Book renewed. New due date: " + newDueDate +
". Renewals remaining: " + (2 - transaction.getRenewalCount()))
.build();
}
}Fine Calculator Utility
Transaction Controller
package com.wohotech.library.controller;
import com.wohotech.library.dto.*;
import com.wohotech.library.service.TransactionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/transactions")
@RequiredArgsConstructor
@Tag(name = "Library Transactions", description = "Book issue, return, and renewal operations")
public class TransactionController {
private final TransactionService transactionService;
@PostMapping("/issue")
@Operation(summary = "Issue a book to a member")
public ResponseEntity<TransactionResponse> issueBook(
@Valid @RequestBody IssueRequest request) {
return ResponseEntity.ok(transactionService.issueBook(request));
}
@PostMapping("/return")
@Operation(summary = "Return an issued book")
public ResponseEntity<TransactionResponse> returnBook(
@Valid @RequestBody ReturnRequest request) {
return ResponseEntity.ok(transactionService.returnBook(request));
}
@PostMapping("/renew/{barcode}")
@Operation(summary = "Renew an issued book")
public ResponseEntity<TransactionResponse> renewBook(
@PathVariable String barcode,
@RequestParam String memberId) {
return ResponseEntity.ok(transactionService.renewBook(barcode, memberId));
}
@GetMapping("/member/{memberId}/current")
@Operation(summary = "Get currently issued books for a member")
public ResponseEntity<List<TransactionResponse>> getCurrentIssues(
@PathVariable String memberId) {
return ResponseEntity.ok(transactionService.getCurrentIssues(memberId));
}
@GetMapping("/member/{memberId}/history")
@Operation(summary = "Get complete borrowing history")
public ResponseEntity<List<TransactionResponse>> getHistory(
@PathVariable String memberId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return ResponseEntity.ok(transactionService.getHistory(memberId, page, size));
}
@GetMapping("/overdue")
@Operation(summary = "Get all overdue books")
public ResponseEntity<List<TransactionResponse>> getOverdueBooks() {
return ResponseEntity.ok(transactionService.getOverdueBooks());
}
}Book Repository
package com.wohotech.library.repository;
import com.wohotech.library.entity.Book;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
Optional<Book> findByIsbn(String isbn);
Page<Book> findByCategoryId(Long categoryId, Pageable pageable);
@Query("SELECT b FROM Book b WHERE " +
"LOWER(b.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
"LOWER(b.isbn) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
"EXISTS (SELECT a FROM b.authors a WHERE LOWER(a.name) LIKE LOWER(CONCAT('%', :keyword, '%')))")
Page<Book> searchBooks(@Param("keyword") String keyword, Pageable pageable);
@Query("SELECT b FROM Book b WHERE b.availableCopies > 0 ORDER BY b.createdAt DESC")
List<Book> findAvailableBooks(Pageable pageable);
@Query("SELECT b FROM Book b JOIN b.copies bc JOIN Transaction t ON t.bookCopy = bc " +
"GROUP BY b ORDER BY COUNT(t) DESC")
List<Book> findMostBorrowedBooks(Pageable pageable);
@Query("SELECT b.category.name, COUNT(b) FROM Book b GROUP BY b.category.name")
List<Object[]> getBookCountByCategory();
long countByAvailableCopiesGreaterThan(int count);
}API Endpoints
| Method | URL | Description | Auth |
|---|---|---|---|
| GET | /api/v1/books | List all books (paginated) | Any |
| GET | /api/v1/books/{id} | Get book details | Any |
| GET | /api/v1/books/search?q= | Search books | Any |
| POST | /api/v1/books | Add new book | Librarian |
| PUT | /api/v1/books/{id} | Update book | Librarian |
| DELETE | /api/v1/books/{id} | Remove book | Librarian |
| POST | /api/v1/members | Register member | Librarian |
| GET | /api/v1/members/{memberId} | Get member details | Any |
| POST | /api/v1/transactions/issue | Issue book | Librarian |
| POST | /api/v1/transactions/return | Return book | Librarian |
| POST | /api/v1/transactions/renew/{barcode} | Renew book | Member |
| GET | /api/v1/transactions/member/{id}/current | Current issues | Member |
| GET | /api/v1/transactions/overdue | Overdue books list | Librarian |
| POST | /api/v1/reservations | Reserve a book | Member |
| GET | /api/v1/reservations/member/{id} | My reservations | Member |
| GET | /api/v1/reports/dashboard | Library dashboard | Librarian |
| GET | /api/v1/reports/monthly | Monthly report | Librarian |
| GET | /api/v1/categories | List categories | Any |
How to Run
Prerequisites
- Java 17+, Maven 3.9+, MySQL 8.0
Setup Steps
Step 1: Clone and create database
git clone https://github.com/wohotech/library-management-system.git
cd library-management-systemCREATE DATABASE library_db;Step 2: Configure application.yml
Step 3: Build and run
mvn clean install
mvn spring-boot:runStep 4: Access at http://localhost:8080/swagger-ui.html
Screenshots Description
- Book Catalog — Grid view of books with cover images, availability badge
- Book Issue Form — Scan barcode, select member, shows due date
- Return with Fine — Return confirmation showing overdue days and fine amount
- Member Dashboard — Currently issued books, due dates, borrowing history
- Search Results — Searching by author name showing matching books
- Overdue Report — List of overdue books with member details and days overdue
- Category Tree — Hierarchical category view with book counts
Future Enhancements
- RFID Integration — Automated book detection and self-checkout kiosks
- Digital Library — E-book and PDF lending with DRM protection
- Inter-Library Loan — Request books from partner libraries
- Mobile App — Member app for search, reserve, and renew
- Analytics Dashboard — Reading trends, popular genres, peak hours
- QR Code — QR-based self-service issue/return
- AI Recommendations — Suggest books based on borrowing history
- Multi-branch — Support for multiple library branches
- Late Return Prediction — ML-based prediction for likely overdue returns
- Integration — OPAC (Online Public Access Catalog) standard compliance
Interview Questions
Q1: How do you handle concurrent book issues (two people trying to issue the last copy)?
Answer: We use pessimistic locking on the available_copies column. The issueBook() method uses @Transactional and @Lock(LockModeType.PESSIMISTIC_WRITE) on the query that reads available copies. The first transaction gets the lock and decrements the count; the second waits and then sees 0 available, throwing BookNotAvailableException.
Q2: Why did you separate Book and BookCopy entities?
Answer: A single book title (ISBN) can have multiple physical copies. The Book entity represents the title metadata (author, publisher, ISBN), while BookCopy represents each physical unit with its own barcode, condition, and status. This allows tracking individual copy status (some copies may be damaged while others are available) and supports multi-copy management efficiently.
Q3: How does the reservation queue work?
Answer: When a member reserves an unavailable book, a Reservation record is created with a queue position (incrementing). When the book is returned, the system checks for pending reservations, notifies the first member in queue via email, and gives them 48 hours to pick up. If they don't, the reservation expires and the next person is notified. We use a FIFO queue with position tracking.
Q4: How is the fine calculation designed for extensibility?
Answer: The FineCalculator is a separate @Component with configurable rates from application.yml. Adding a new member type or changing rates requires no code changes — just update the config. The calculator uses the Strategy pattern (member type determines rate). It's also unit-testable independently of the service layer. A maximum fine cap prevents unreasonably large fines.
Q5: How do you prevent a member from issuing books if they have overdue items?
Answer: Before issuing, we query transactionRepository.getPendingFines(memberId) which checks for all transactions where return_date IS NULL AND due_date < CURRENT_DATE. If any pending fine exists (sum > 0), we throw OverdueBooksException. This enforces responsibility and ensures the library doesn't lose track of books.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Library Management System — Java Programming.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, projects, library, management
Related Java Master Course Topics