Browse Templates/Library Management System

Library Management System

A complete library management database schema designed for public libraries, school libraries, or digital lending platforms. Includes book catalog with categories, member registration, loan tracking with due dates and late fees, and a reservation system for in-demand titles. Suitable for building library management software, digital lending platforms, or book inventory systems.

PostgreSQL10 tablesEducation
librarybooksloansreservationsmemberseducation
Created with ERD Studio

About this schema

A book and a copy of a book are different things, and almost every decision here follows from that. books is the title with its ISBN and publisher; book_copies is the physical object on the shelf, with a barcode, a condition and a location. You cannot lend a title — loans keys on copy_id, because what leaves the building is one specific object.

Reservations key on the title instead. reservations points at book_id, because a member waiting for a popular novel does not care which copy they get — the queue is for the work, and the system assigns whichever copy comes back first.

Fines reference both the loan and the member. The loan explains what the fine is for and the member is who owes it, which matters because a fine outlives the loan it came from: the book goes back, the debt stays until is_paid says otherwise.

Key relationships

books → book_copies → loans
Title, physical copy, then the lending record. Only the copy can be borrowed, which is what makes three copies of one book lendable at once.
books ↔ authors via book_authors
Many-to-many with author_role, so an editor, a translator and a co-author are all representable without separate tables.
books → reservations ← members
The hold queue is on the title, not the copy, with expires_at so an uncollected reservation releases itself.
loans → fines ← members
The fine names the loan that caused it and the member who owes it, so a debt survives the book being returned.
categories → categories (parent_id)
A self-referencing classification tree, so a subject hierarchy deepens without a schema change.
members.expires_at / membership_type
Borrowing rights are time-bounded, so a lapsed membership blocks a loan without the row being deleted.

Design decisions

Loans point at copies, reservations at titles

This asymmetry is the heart of the schema. Lending has to name the exact object so its condition, location and return are traceable. Reserving must not, because a hold on copy number three would fail when copies one and two come back first. Modelling both the same way breaks one of them — the split is what makes the queue work while keeping the circulation record precise.

Fines are rows, not a balance on the member

A single amount_owed column would be smaller and would answer nothing: which book, how late, has it been appealed. Each fine keeps its reason, its amount and is_paid, so a member's balance is a sum over unpaid rows. The cost is that the balance is computed rather than read, which for a number this small is not a cost at all.

Both the loan and the copy carry status

The copy's status tracks the object — available, on loan, lost, in repair. The loan's status tracks the transaction — active, returned, overdue. A copy can be lost while its loan is still open, and a loan can be closed while the copy goes straight into repair. One column could not say either.

returned_at is nullable rather than a separate table

An open loan is one with a null returned_at, which makes the overdue query a comparison against due_date with no join. Moving returns into their own table would be more normalised and would turn the most frequent question a library asks — what is out right now — into an anti-join. The null is doing real work here.

Tables in this template

categoriesCategories
5 cols
authorsAuthors
5 cols
booksBooks
9 cols
book_authorsBook Authors
4 cols
book_copiesBook Copies
7 cols
membersMembers
8 cols
loansLoans
7 cols
reservationsReservations
6 cols
finesFines
7 cols
librariansLibrarians
6 cols

Frequently asked

How do I find overdue loans?
Rows where returned_at is null and due_date is in the past. The status column is a convenience that must be kept in step; the two timestamps are what actually define overdue.
Why does a copy need a barcode when it has an id?
Because the id is internal and the barcode is on the physical label that gets scanned at the desk. It also survives data migrations — the sticker on the book cannot be reissued because the database changed.
How does a reservation turn into a loan?
When a copy of that title is returned, the oldest active reservation is matched to it and a loan is created against that copy. The reservation's expires_at is what stops a held book sitting on the shelf indefinitely.
What happens when a copy is lost?
The copy's status changes and the loan is usually closed with a fine attached. The copy row stays — deleting it would orphan the loan history and hide the fact that the library once held four copies and now holds three.
Why is librarians not linked to loans?
It is a gap. Nothing records who issued or received a book, so a disputed return has no staff record behind it. Adding issued_by and received_by to loans is the first change for a library that needs accountability at the desk.