Building a Feature-Rich Library Management System with Django
Library management might sound boring, but wait until you hear about peer-to-peer book lending, automated email reminders, and digital ID cards. This was my introduction to real-world software engineering, and it taught me more about building production systems than any tutorial could.
The Vision
Most library systems are stuck in the 90s - basic check-in/check-out with zero user experience. We wanted to build something modern:
- Users request books from other users (peer lending)
- Buy used books from the library or other students
- Automated renewal reminders
- Digital library ID cards (generated as PDF)
- Book ratings and reviews
- Dynamic search with filters
Architecture and Tech Stack
Tech Stack:
├── Backend: Django 4.x + Django REST Framework
├── Database: PostgreSQL with complex relational schema
├── Frontend: Django Templates + Bootstrap 5
├── Search: PostgreSQL full-text search
├── Email: SMTP with async task queue
├── PDF Generation: ReportLab
└── Deployment: Nginx + Gunicorn on Ubuntu
Database Schema Design
This is where things got interesting. A simple library has users and books, but we needed to handle:
- Multiple copy states (available, checked out, reserved)
- User-to-user book requests
- Books for sale marketplace
- Fine calculation
- Transaction history
Core Models
class Book(models.Model):
title = models.CharField(max_length=200)
isbn = models.CharField(max_length=13, unique=True)
author = models.CharField(max_length=100)
copies_total = models.IntegerField()
copies_available = models.IntegerField()
category = models.ForeignKey(Category)
is_for_sale = models.BooleanField(default=False)
sale_price = models.DecimalField(null=True, blank=True)
def can_checkout(self):
return self.copies_available > 0
def get_average_rating(self):
return self.reviews.aggregate(Avg('rating'))['rating__avg']
class BookTransaction(models.Model):
TRANSACTION_TYPES = [
('checkout', 'Library Checkout'),
('user_request', 'User Request'),
('purchase', 'Purchase'),
]
book = models.ForeignKey(Book)
borrower = models.ForeignKey(User, related_name='borrowed_books')
lender = models.ForeignKey(User, related_name='lent_books', null=True)
transaction_type = models.CharField(choices=TRANSACTION_TYPES)
checkout_date = models.DateTimeField(auto_now_add=True)
due_date = models.DateTimeField()
return_date = models.DateTimeField(null=True)
fine_amount = models.DecimalField(default=0)
def calculate_fine(self):
if self.return_date and self.return_date > self.due_date:
days_late = (self.return_date - self.due_date).days
return days_late * 5 # $5 per day
return 0
The Tricky Part: Book Availability
Managing book availability across three lending modes was complex:
def checkout_book(book_id, user):
with transaction.atomic():
book = Book.objects.select_for_update().get(id=book_id)
if book.copies_available < 1:
raise ValidationError("No copies available")
# Atomic update
book.copies_available = F('copies_available') - 1
book.save()
BookTransaction.objects.create(
book=book,
borrower=user,
due_date=timezone.now() + timedelta(days=14)
)
select_for_update() prevents race conditions - critical when multiple users might request the last copy simultaneously.
User Features
1. Peer-to-Peer Book Requests
Users can see who has a book checked out and request it:
class BookRequest(models.Model):
book = models.ForeignKey(Book)
requester = models.ForeignKey(User, related_name='requests_made')
current_holder = models.ForeignKey(User, related_name='requests_received')
status = models.CharField(choices=[
('pending', 'Pending'),
('approved', 'Approved'),
('declined', 'Declined'),
])
message = models.TextField()
def approve(self):
with transaction.atomic():
self.status = 'approved'
self.save()
# Transfer book
old_transaction = BookTransaction.objects.get(
book=self.book,
borrower=self.current_holder,
return_date__isnull=True
)
old_transaction.return_date = timezone.now()
old_transaction.save()
# New transaction
BookTransaction.objects.create(
book=self.book,
borrower=self.requester,
lender=self.current_holder,
transaction_type='user_request',
due_date=timezone.now() + timedelta(days=14)
)
2. Automated Email Notifications
Django Celery for async email tasks:
from celery import shared_task
from django.core.mail import send_mail
from django.utils import timezone
@shared_task
def send_renewal_reminders():
"""Run daily to remind users of due dates"""
tomorrow = timezone.now() + timedelta(days=1)
due_soon = BookTransaction.objects.filter(
return_date__isnull=True,
due_date__date=tomorrow.date()
)
for transaction in due_soon:
send_mail(
subject=f'Book Due Tomorrow: {transaction.book.title}',
message=f'Hi {transaction.borrower.username},\n\n'
f'Your book "{transaction.book.title}" is due tomorrow.\n'
f'Please return or renew it to avoid fines.',
from_email='library@example.com',
recipient_list=[transaction.borrower.email],
)
Celery Beat schedules this to run daily at 8 AM.
3. Digital Library Cards
PDF generation with ReportLab:
from reportlab.lib.pagesizes import letter, A7
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
import qrcode
def generate_library_card(user):
buffer = BytesIO()
c = canvas.Canvas(buffer, pagesize=A7)
# QR code with user ID
qr = qrcode.make(f'USER-{user.id}')
qr_buffer = BytesIO()
qr.save(qr_buffer)
qr_image = ImageReader(qr_buffer)
# Draw card
c.drawImage(qr_image, 10, 150, width=80, height=80)
c.setFont("Helvetica-Bold", 14)
c.drawString(100, 200, user.get_full_name())
c.setFont("Helvetica", 10)
c.drawString(100, 180, f"ID: {user.library_id}")
c.drawString(100, 165, f"Expires: {user.card_expiry}")
c.save()
return buffer.getvalue()
Admin Dashboard
Custom Django admin with rich features:
@admin.register(BookTransaction)
class BookTransactionAdmin(admin.ModelAdmin):
list_display = ['book', 'borrower', 'transaction_type',
'checkout_date', 'due_date', 'is_overdue']
list_filter = ['transaction_type', 'checkout_date']
search_fields = ['book__title', 'borrower__username']
actions = ['mark_returned', 'send_reminders']
def is_overdue(self, obj):
if not obj.return_date and obj.due_date < timezone.now():
return True
return False
is_overdue.boolean = True
def mark_returned(self, request, queryset):
for transaction in queryset:
transaction.return_date = timezone.now()
transaction.fine_amount = transaction.calculate_fine()
transaction.save()
# Update book availability
book = transaction.book
book.copies_available = F('copies_available') + 1
book.save()
Search and Filtering
Dynamic search with multiple fields:
def search_books(query, filters):
qs = Book.objects.all()
if query:
qs = qs.filter(
Q(title__icontains=query) |
Q(author__icontains=query) |
Q(isbn__icontains=query)
)
if filters.get('category'):
qs = qs.filter(category_id=filters['category'])
if filters.get('available_only'):
qs = qs.filter(copies_available__gt=0)
if filters.get('for_sale'):
qs = qs.filter(is_for_sale=True)
return qs.select_related('category').prefetch_related('reviews')
Performance Optimizations
Initial load time: 2.3s
After optimization: 0.4s
What We Did:
- Database Indexing
class Book(models.Model):
# ...
class Meta:
indexes = [
models.Index(fields=['title', 'author']),
models.Index(fields=['category', 'copies_available']),
]
- Query Optimization
- Used
select_related()for foreign keys - Used
prefetch_related()for many-to-many -
Avoided N+1 queries
-
Caching
from django.core.cache import cache
def get_popular_books():
cache_key = 'popular_books'
books = cache.get(cache_key)
if books is None:
books = Book.objects.annotate(
checkout_count=Count('booktransaction')
).order_by('-checkout_count')[:10]
cache.set(cache_key, books, 3600) # 1 hour
return books
Deployment Challenges
Challenge 1: Email Configuration
- Development: Console backend
- Production: SMTP with Gmail
- Solution: Environment-based settings
Challenge 2: Static Files
- Used WhiteNoise for efficient static file serving
- CDN for user-uploaded book covers
Challenge 3: Database Backups
- Automated daily PostgreSQL backups
- Stored on S3 with 30-day retention
Results and Impact
- Users: 500+ students
- Books: 10,000+ in catalog
- Transactions: 3,000+ checkouts
- Peer requests: 200+ successful transfers
- Satisfaction: 4.6/5 average rating
Key Learnings
- Database design is critical - Poor schema causes issues later
- Atomic transactions prevent bugs - Race conditions are real
- Email is harder than it looks - Deliverability, rate limits, templates
- Users find edge cases - Had to handle books lost, damaged, etc.
- Performance matters - 2s load times = users leave
GitHub
Full source code: github.com/jithendra1798/SE-Project
This project taught me that software engineering is about solving real problems, not just writing code.