Building a Library Management System in Java with SQL
Building a robust library management system requires careful database design, transaction management, and implementing complex business rules. This comprehensive guide details how we built a production-ready system using Java and MySQL that handles thousands of daily transactions, implements advanced features like reservation queues, automatic fine calculation, and real-time availability tracking.
System Overview
A production-ready library management system needs to handle several key operations:
Architecture and Design Patterns
The system follows the DAO (Data Access Object) pattern for clean separation of concerns:
Database Design
The foundation of any good library system is its database schema. Here's our optimized schema with proper indexing:
-- Users table
CREATE TABLE users (
    user_id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Books table
CREATE TABLE books (
    book_id INT PRIMARY KEY AUTO_INCREMENT,
    isbn VARCHAR(13) UNIQUE NOT NULL,
    title VARCHAR(200) NOT NULL,
    author VARCHAR(100) NOT NULL,
    publication_year INT,
    available_copies INT DEFAULT 1,
    total_copies INT DEFAULT 1,
    INDEX idx_title (title),
    INDEX idx_author (author)
);
-- Loans table
CREATE TABLE loans (
    loan_id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    book_id INT,
    loan_date DATE NOT NULL,
    due_date DATE NOT NULL,
    return_date DATE,
    FOREIGN KEY (user_id) REFERENCES users(user_id),
    FOREIGN KEY (book_id) REFERENCES books(book_id)
);
-- Reviews table
CREATE TABLE reviews (
    review_id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    book_id INT,
    rating INT CHECK (rating >= 1 AND rating <= 5),
    review_text TEXT,
    review_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(user_id),
    FOREIGN KEY (book_id) REFERENCES books(book_id)
);Java Implementation with Advanced Features
Let's look at the core Java classes with connection pooling and transaction management:
// Connection Pool Configuration
public class DatabaseConfig {
    private static HikariDataSource dataSource;
    
    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/library");
        config.setUsername("library_user");
        config.setPassword("secure_password");
        config.setMaximumPoolSize(10);
        config.setMinimumIdle(2);
        config.setConnectionTimeout(30000);
        dataSource = new HikariDataSource(config);
    }
    
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
}
// Enhanced Book Entity
public class Book {
    private int bookId;
    private String isbn;
    private String title;
    private String author;
    private int publicationYear;
    private int availableCopies;
    private int totalCopies;
    private double averageRating;
    
    // Constructor, getters/setters, equals, hashCode
}
// DAO with Transaction Management
public class LibraryDAO {
    
    public boolean loanBook(int userId, int bookId) throws SQLException {
        Connection conn = null;
        PreparedStatement checkStmt = null;
        PreparedStatement loanStmt = null;
        PreparedStatement updateStmt = null;
        
        try {
            conn = DatabaseConfig.getConnection();
            conn.setAutoCommit(false); // Start transaction
            
            // Check if book is available
            String checkQuery = "SELECT available_copies FROM books WHERE book_id = ? FOR UPDATE";
            checkStmt = conn.prepareStatement(checkQuery);
            checkStmt.setInt(1, bookId);
            ResultSet rs = checkStmt.executeQuery();
            
            if (rs.next() && rs.getInt("available_copies") > 0) {
                // Create loan record
                String loanQuery = "INSERT INTO loans (user_id, book_id, loan_date, due_date) " +
                                  "VALUES (?, ?, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 14 DAY))";
                loanStmt = conn.prepareStatement(loanQuery);
                loanStmt.setInt(1, userId);
                loanStmt.setInt(2, bookId);
                loanStmt.executeUpdate();
                
                // Update available copies
                String updateQuery = "UPDATE books SET available_copies = available_copies - 1 " +
                                   "WHERE book_id = ?";
                updateStmt = conn.prepareStatement(updateQuery);
                updateStmt.setInt(1, bookId);
                updateStmt.executeUpdate();
                
                conn.commit();
                return true;
            }
            
            conn.rollback();
            return false;
            
        } catch (SQLException e) {
            if (conn != null) conn.rollback();
            throw e;
        } finally {
            // Close resources in reverse order
            if (updateStmt != null) updateStmt.close();
            if (loanStmt != null) loanStmt.close();
            if (checkStmt != null) checkStmt.close();
            if (conn != null) conn.close();
        }
    }
    
    // Advanced search with pagination
    public List<Book> searchBooks(String searchTerm, int offset, int limit) throws SQLException {
        String query = "SELECT b.*, AVG(r.rating) as avg_rating " +
                      "FROM books b " +
                      "LEFT JOIN reviews r ON b.book_id = r.book_id " +
                      "WHERE b.title LIKE ? OR b.author LIKE ? " +
                      "GROUP BY b.book_id " +
                      "ORDER BY avg_rating DESC NULLS LAST " +
                      "LIMIT ? OFFSET ?";
        
        try (Connection conn = DatabaseConfig.getConnection();
             PreparedStatement stmt = conn.prepareStatement(query)) {
            
            String searchPattern = "%" + searchTerm + "%";
            stmt.setString(1, searchPattern);
            stmt.setString(2, searchPattern);
            stmt.setInt(3, limit);
            stmt.setInt(4, offset);
            
            ResultSet rs = stmt.executeQuery();
            List<Book> books = new ArrayList<>();
            
            while (rs.next()) {
                Book book = new Book();
                book.setBookId(rs.getInt("book_id"));
                book.setIsbn(rs.getString("isbn"));
                book.setTitle(rs.getString("title"));
                book.setAuthor(rs.getString("author"));
                book.setAverageRating(rs.getDouble("avg_rating"));
                books.add(book);
            }
            
            return books;
        }
    }
    
    // Review system with validation
    public void addReview(int userId, int bookId, int rating, String reviewText) throws SQLException {
        // Verify user has borrowed the book
        String verifyQuery = "SELECT COUNT(*) FROM loans WHERE user_id = ? AND book_id = ? AND return_date IS NOT NULL";
        
        try (Connection conn = DatabaseConfig.getConnection()) {
            PreparedStatement verifyStmt = conn.prepareStatement(verifyQuery);
            verifyStmt.setInt(1, userId);
            verifyStmt.setInt(2, bookId);
            ResultSet rs = verifyStmt.executeQuery();
            
            if (rs.next() && rs.getInt(1) > 0) {
                String insertQuery = "INSERT INTO reviews (user_id, book_id, rating, review_text) VALUES (?, ?, ?, ?)";
                PreparedStatement insertStmt = conn.prepareStatement(insertQuery);
                insertStmt.setInt(1, userId);
                insertStmt.setInt(2, bookId);
                insertStmt.setInt(3, rating);
                insertStmt.setString(4, reviewText);
                insertStmt.executeUpdate();
            } else {
                throw new SQLException("User must borrow and return the book before reviewing");
            }
        }
    }
}Advanced Features and Best Practices
1. Overdue Book Tracking with Scheduled Jobs
@Component
public class OverdueBookScheduler {
    
    @Scheduled(cron = "0 0 9 * * ?") // Run daily at 9 AM
    public void checkOverdueBooks() {
        String query = "SELECT l.*, u.email, b.title " +
                      "FROM loans l " +
                      "JOIN users u ON l.user_id = u.user_id " +
                      "JOIN books b ON l.book_id = b.book_id " +
                      "WHERE l.return_date IS NULL AND l.due_date < CURDATE()";
        
        try (Connection conn = DatabaseConfig.getConnection();
             PreparedStatement stmt = conn.prepareStatement(query)) {
            
            ResultSet rs = stmt.executeQuery();
            while (rs.next()) {
                sendOverdueNotification(
                    rs.getString("email"),
                    rs.getString("title"),
                    rs.getDate("due_date")
                );
            }
        } catch (SQLException e) {
            logger.error("Failed to check overdue books", e);
        }
    }
}2. Performance Optimization
3. Security Considerations
// Input validation example
public class ValidationUtils {
    public static boolean isValidISBN(String isbn) {
        return isbn != null && isbn.matches("^(97[89])?\d{9}(\d|X)$");
    }
    
    public static String sanitizeInput(String input) {
        return input.replaceAll("[<>"']", "");
    }
}
// Authentication example
public class AuthService {
    public User authenticate(String username, String password) throws SQLException {
        String query = "SELECT * FROM users WHERE username = ?";
        
        try (Connection conn = DatabaseConfig.getConnection();
             PreparedStatement stmt = conn.prepareStatement(query)) {
            
            stmt.setString(1, username);
            ResultSet rs = stmt.executeQuery();
            
            if (rs.next()) {
                String hashedPassword = rs.getString("password_hash");
                if (BCrypt.checkpw(password, hashedPassword)) {
                    return mapResultSetToUser(rs);
                }
            }
        }
        return null;
    }
}4. RESTful API Design
@RestController
@RequestMapping("/api/books")
public class BookController {
    
    @Autowired
    private LibraryService libraryService;
    
    @GetMapping("/search")
    public ResponseEntity<List<Book>> searchBooks(
            @RequestParam String query,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        List<Book> books = libraryService.searchBooks(query, page * size, size);
        return ResponseEntity.ok(books);
    }
    
    @PostMapping("/{bookId}/loan")
    public ResponseEntity<LoanResponse> loanBook(
            @PathVariable int bookId,
            @AuthenticationPrincipal User user) {
        
        try {
            boolean success = libraryService.loanBook(user.getId(), bookId);
            if (success) {
                return ResponseEntity.ok(new LoanResponse("Book loaned successfully"));
            } else {
                return ResponseEntity.badRequest().body(new LoanResponse("Book not available"));
            }
        } catch (SQLException e) {
            return ResponseEntity.internalServerError().body(new LoanResponse("Database error"));
        }
    }
}Deployment Considerations
Conclusion
This library management system demonstrates enterprise-level Java development with:
Technical Achievements:
Advanced Features Implemented:
Lessons Learned:
The complete source code demonstrates professional Java development practices and can serve as a foundation for commercial library systems.
About the Author
The Nordic Oculus team brings together skills in modern software development, cloud architecture, and emerging technologies.
Learn more about our team