Skip to content

Building a Feature-Rich Library Management System with Django

Full-stack development of a modern library platform with book requests, reviews, and automated notifications

Jithendra Puppala
Jithendra Puppala
6 min read 19 views
Building a Feature-Rich Library Management System with Django
Tech Stack: Django PostgreSQL JavaScript Tailwind CSS Redis

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:

  1. Database Indexing
class Book(models.Model):
    # ...
    class Meta:
        indexes = [
            models.Index(fields=['title', 'author']),
            models.Index(fields=['category', 'copies_available']),
        ]
  1. Query Optimization
  2. Used select_related() for foreign keys
  3. Used prefetch_related() for many-to-many
  4. Avoided N+1 queries

  5. 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

  1. Database design is critical - Poor schema causes issues later
  2. Atomic transactions prevent bugs - Race conditions are real
  3. Email is harder than it looks - Deliverability, rate limits, templates
  4. Users find edge cases - Had to handle books lost, damaged, etc.
  5. 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.

Get In Touch

I'll respond within 24-48 hours