Web-based Library Information System

A library information system is a classic must-know project, yet many beginners oversimplify it — for example building just a book CRUD without handling availability, due dates, an...

Web-based Library Information System

A library information system is a classic must-know project, yet many beginners oversimplify it — for example building just a book CRUD without handling availability, due dates, and fines. But that is exactly where the real business logic lives. This tutorial builds a proper library in Laravel 11 with three core entities and a safe borrow-return flow.

1. Designing the Entities

We need three tables: books (book data plus number of copies), members, and loans (borrowing transactions). Key design: each title can have several copies, so availability is computed from total copies minus outstanding (not-yet-returned) loans.

php artisan make:model Book -m
php artisan make:model Member -m
php artisan make:model Loan -m

The books and members migrations

Schema::create('books', function (Blueprint $table) {
    $table->id();
    $table->string('isbn')->unique()->nullable();
    $table->string('title');
    $table->string('author');
    $table->unsignedInteger('copies')->default(1); // total copies
    $table->timestamps();
});

Schema::create('members', function (Blueprint $table) {
    $table->id();
    $table->string('code')->unique(); // membership number
    $table->string('name');
    $table->string('phone')->nullable();
    $table->timestamps();
});

The loans migration

The loans table stores the borrow date, due date, and return date. As long as returned_at is still null, the book is considered on loan.

Schema::create('loans', function (Blueprint $table) {
    $table->id();
    $table->foreignId('book_id')->constrained();
    $table->foreignId('member_id')->constrained();
    $table->date('borrowed_at');
    $table->date('due_at');
    $table->date('returned_at')->nullable();
    $table->unsignedInteger('fine')->default(0);
    $table->timestamps();
});
php artisan migrate

2. Models and Availability Check

On the Book model, we count how many copies are currently on loan, then availability = total copies minus those on loan. This prevents a book from being borrowed beyond its number of copies.

class Book extends Model
{
    protected $fillable = ['isbn', 'title', 'author', 'copies'];

    public function loans()
    {
        return $this->hasMany(Loan::class);
    }

    public function getOnLoanAttribute(): int
    {
        return $this->loans()->whereNull('returned_at')->count();
    }

    public function getAvailableAttribute(): int
    {
        return max(0, $this->copies - $this->on_loan);
    }
}

The Loan model stores the business rules — loan duration and daily fine rate — as constants so they are easy to change in one place:

class Loan extends Model
{
    const LOAN_DAYS = 7;       // standard loan duration
    const FINE_PER_DAY = 1000; // fine per late day

    protected $fillable = ['book_id', 'member_id', 'borrowed_at', 'due_at', 'returned_at', 'fine'];

    protected $casts = [
        'borrowed_at' => 'date',
        'due_at'      => 'date',
        'returned_at' => 'date',
    ];
}

3. The Borrowing Flow

When a member borrows, we check availability first, then create a transaction with an automatic due date. Use DB::transaction and lockForUpdate so two members cannot grab the last copy at the same time.

use App\Models\Loan;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;

public function borrow(Request $request)
{
    $data = $request->validate([
        'book_id'   => 'required|exists:books,id',
        'member_id' => 'required|exists:members,id',
    ]);

    DB::transaction(function () use ($data) {
        $book = Book::lockForUpdate()->findOrFail($data['book_id']);

        if ($book->available < 1) {
            abort(422, 'All copies of this book are currently on loan.');
        }

        Loan::create([
            'book_id'     => $book->id,
            'member_id'   => $data['member_id'],
            'borrowed_at' => now(),
            'due_at'      => now()->addDays(Loan::LOAN_DAYS),
        ]);
    });

    return back()->with('success', 'Loan recorded successfully.');
}

4. The Return Flow and Fine Calculation

When a book is returned, we compare the return date with the due date. If late, the fine = number of late days multiplied by the daily rate. Note the diffInDays argument order so the lateness comes out positive.

public function returnBook(Loan $loan)
{
    if ($loan->returned_at) {
        return back()->with('error', 'This book has already been returned.');
    }

    $today = Carbon::today();
    $fine = 0;

    if ($today->greaterThan($loan->due_at)) {
        $lateDays = $loan->due_at->diffInDays($today);
        $fine = $lateDays * Loan::FINE_PER_DAY;
    }

    $loan->update([
        'returned_at' => $today,
        'fine'        => $fine,
    ]);

    return back()->with('success', $fine > 0
        ? "Book returned. Fine: Rp " . number_format($fine)
        : 'Book returned on time.');
}

5. Listing Overdue Loans

Staff need to see who is still holding books past the due date. The query simply filters loans that are not yet returned and past their date:

$overdue = Loan::query()
    ->whereNull('returned_at')
    ->whereDate('due_at', '<', now())
    ->with(['book', 'member'])
    ->get();

foreach ($overdue as $loan) {
    $lateDays = $loan->due_at->diffInDays(now());
    // show $loan->member->name, $loan->book->title, $lateDays
}

Common Mistakes to Avoid

  • Storing an "on loan" boolean on the book table: it fails to handle multiple copies. Compute availability from copies minus active loans.
  • Skipping a transaction when borrowing: the last copy can be lent to two people at once.
  • Wrong diffInDays order: make sure you measure from the due date to today so the fine is never negative.

With a three-entity structure and rules stored as constants, your library system is easy to extend: add loan renewals, a maximum number of books per member, or due-date email notifications without reworking the data model.

Yudhi
Written by
Yudhi
Founder & Lead Developer, GudangCode

Yudhi is the founder of GudangCode and a Laravel developer who has built dozens of ready-to-use business information systems — from POS and HRIS to management apps. He writes guides and articles on GudangCode to help Indonesian developers run, understand, and deploy Laravel source code correctly.

LaravelPHPMySQLSistem Informasi Bisnis See all articles by Yudhi
Want the full source code & apps?

Sign up free to download ready-to-use business applications, information systems, and Laravel source code.

Sign Up Free & Download
Sistem Informasi Perpustakaan Sistem Informasi
Share this article
Back to Blog
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →

Get Full Access Now!

Join our membership and unlock exclusive access to all premium features. Fast, easy, and ready to use instantly.

Join Membership Now
Tim Support
Online
Isi data dulu untuk mulai chat:
Beri rating & testimoni sebelum menutup:
Live chat by gudangcode.com