System Design • Full-Stack Architecture

Scalable Alumni Portal: A Hybrid Monolith Strategy

How we architected a platform for 50,000+ alumni using a hybrid Django + Flask topology, partial PostgreSQL indexing, Redis write-through caching, and ACID transaction safety for donations.

Shubham Kulkarni
Shubham Kulkarni Lead Software Engineer & Architect
Published February 2026 • Updated Sep 2026
Engineers whiteboard system design architecture

Building a university alumni platform sounds deceptively pedestrian on paper: "It's just user profiles, a job board, and an events calendar." But when you factor in 50 graduating classes dating back to 1974, complex multi-tenant role permissions, high-frequency donation matching, and virtual reunion events that spike from 50 to 8,000 concurrent users within minutes, standard CRUD assumptions evaporate. During our very first reunion week launch, unindexed full-text queries against 50,000 alumni profiles locked up PostgreSQL, exhausting connection pools and taking the service down.

This case study documents how we redesigned the platform into a Hybrid Monolith-Microservice Architecture. By anchoring core relational data in Django, offloading real-time WebSocket fan-out to a lightweight Flask daemon, deploying targeted partial PostgreSQL indexes, and engineering a Redis write-through cache with pattern invalidation, we dropped P50 query latency to 18ms and survived 8,200 concurrent users on a single $40/month host.

50k+ Active Alumni Records
12ms Directory Query Latency
94% Peak Cache Hit Rate
8.2k Concurrent Users Handled

1. The Scale Problem: Three Conflicting Access Profiles

The core challenge in university community platforms is that different user personas place completely contradictory demands on the database:

  • The Alumni Persona (Read-Heavy & Filter-Intensive): Searches for classmates by graduation year, major, employer, and geographic city (e.g. "All 2018 Computer Science alumni at Microsoft in Seattle"). Requires instant multi-column filtering across 50,000 rows without full-table scans.
  • The Student Persona (High Write-Burst Concurrency): Applies for campus referral postings, RSVPs for mentorship seminars, and submits questions to forums. Spikes heavily during placement season (September–November).
  • The University Admin (Heavy Compute & Aggregates): Generates cross-batch endowment donation reports, exports audit logs, and triggers bulk emails to 10,000+ recipients. These queries lock tables if run on the primary read/write database instance.
Data Integrity Rules

Zero data loss tolerance for alumni financial contributions, strict Role-Based Access Control (RBAC) preventing unverified profiles from contacting alumni, and soft-delete retention (deleted_at timestamps) for university historical archiving.

2. The Hybrid Architecture: Django Core + Flask Real-Time

Rather than introducing full microservice complexity (which introduces distributed tracing, inter-service networking latency, and complex saga transactions), we adopted a Hybrid Monolith:

System Topology & Request Flow

  • TIER 01 Nginx Gateway SSL termination, rate limiting (100 req/min), path routing
  • TIER 02 Django Core Auth, RBAC, REST APIs, Celery workers, relational data
  • TIER 03 Flask Events Socket.IO daemon for reunion chat & live RSVP counters
  • TIER 04 Data Layer PostgreSQL primary + Redis cache / message queue

Cross-service authentication is decoupled via Stateless HMAC-SHA256 JWTs. When a user authenticates at the Django endpoint, the token payload encapsulates their user ID, verification status, and role permissions. The Flask WebSocket service verifies the JWT signature locally using a shared environment secret, eliminating inter-service database lookups on every chat message.

3. Database Optimization & Partial Indexing

In our initial schema, searching alumni filtered by batch_year and company took 3,200ms because PostgreSQL executed sequential scans across all columns. We analyzed query execution plans with EXPLAIN ANALYZE and applied targeted compound and partial indexes:

alumni/models.py Django ORM • Targeted Composite & Partial Indexes
from django.db import models
from django.db.models import Q
from django.contrib.auth.models import User

class AlumniProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    batch_year = models.IntegerField(db_index=True)
    department = models.CharField(max_length=50)
    company = models.CharField(max_length=100, blank=True)
    city = models.CharField(max_length=100, blank=True)
    industry = models.CharField(max_length=100)
    is_verified = models.BooleanField(default=False)
    deleted_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        indexes = [
            # Compound Index: Matches 85% of directory filter queries
            models.Index(
                fields=['batch_year', 'company'],
                name='batch_company_idx'
            ),
            # Partial Index: Indexes ONLY verified, non-deleted alumni profiles.
            # Reduces index footprint by 38% and avoids indexing inactive rows.
            models.Index(
                fields=['batch_year', 'department'],
                name='verified_active_idx',
                condition=Q(is_verified=True, deleted_at__isnull=True)
            ),
        ]
        ordering = ['-batch_year']

Query Optimization Benchmarks

Query Operation Unindexed (Baseline) Optimized (Partial Index) Speedup
Directory search (batch + company) 3,200 ms 12 ms 266x faster
Verified alumni directory browse 1,800 ms 8 ms 225x faster
Full-text skill / bio search 4,500 ms 45 ms 100x faster
Annual donation aggregate sum 12,000 ms 350 ms 34x faster

4. Caching Strategy: Redis Write-Through with Invalidation

Indexes optimized database reads, but hitting PostgreSQL for static directory views still depleted connection pools. We wrapped repetitive directory queries in a Redis write-through cache decorator with programmatic signal-based cache purging:

alumni/cache_layer.py Python 3.11 / Redis / Smart Invalidation
import redis
import json
import hashlib
from functools import wraps
from django.db.models.signals import post_save
from django.dispatch import receiver

r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
CACHE_TTL = 300  # 5-minute time-to-live

def cache_query(key_prefix: str, ttl: int = CACHE_TTL):
    """Decorator: Caches expensive database reads into Redis."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Compute unique cache key based on query arguments
            raw_key = f"{key_prefix}:{str(args)}:{str(kwargs)}"
            cache_key = f"{key_prefix}:{hashlib.md5(raw_key.encode()).hexdigest()}"
            
            cached = r.get(cache_key)
            if cached:
                return json.loads(cached)  # Cache HIT: 0ms DB time
            
            result = func(*args, **kwargs)
            r.setex(cache_key, ttl, json.dumps(result))
            return result
        return wrapper
    return decorator

# Automatically purge stale directory keys on profile updates
@receiver(post_save, sender=AlumniProfile)
def invalidate_alumni_cache(sender, instance, **kwargs):
    # Scan and purge all cached query variants for this prefix
    for key in r.scan_iter(match="alumni_search:*"):
        r.delete(key)

5. ACID Financial Integrity with SELECT FOR UPDATE

Handling alumni donations requires zero tolerance for race conditions. If two batchmates donate ₹50,000 simultaneously toward a batch endowment goal, concurrent updates could overwrite the running fund counter.

We wrapped webhook ingest in transaction.atomic() paired with row-level pessimistic locking via select_for_update():

donations/views.py ACID Transaction with Pessimistic Locking
from django.db import transaction
from django.http import JsonResponse
import json

@transaction.atomic
def process_donation_webhook(payment_id: str, fund_id: int, amount: float, alumni_id: int):
    """
    Executes all financial mutations in a single atomic transaction.
    If receipt generation or counter updates fail, all steps roll back.
    """
    # 1. Create immutable audit ledger record
    donation = DonationRecord.objects.create(
        alumni_id=alumni_id,
        amount=amount,
        payment_gateway_ref=payment_id,
        status="CONFIRMED"
    )

    # 2. Acquire pessimistic row-lock on fund counter (prevents lost updates)
    fund = EndowmentFund.objects.select_for_update().get(id=fund_id)
    fund.current_balance += amount
    fund.save()

    # 3. Queue asynchronous PDF tax receipt generation in Celery
    generate_tax_receipt.delay(donation.id)
    
    return {"status": "SUCCESS", "donation_id": donation.id}

6. Flask WebSocket Event Architecture

Reunion chat streams and live RSVP attendance counters run on a separate Flask service powered by Socket.IO. This cleanly isolates long-lived persistent WebSocket connections from Django's synchronous WSGI worker pool, preventing connection starvation.

7. Locust Stress Benchmarks & Results

Prior to launch, we simulated Alumni Reunion Day traffic using Locust across a single 4 vCPU, 8 GB RAM server running Docker Compose:

Peak Concurrent Users
8,200
Sustained over 30 mins
P50 Latency
18ms
Target was <100ms
P99 Latency
210ms
Target was <500ms
Error Rate
0.02%
Zero dropped donations
Query Response Latency Across Architecture Iterations
Naive Monolith (Crash) 3,200ms
With Partial Indexes 180ms
Hybrid + Redis (Ours) 18ms

8. Architectural Lessons & Takeaways

TAKEAWAY 01

Index Query Patterns, Not Column Schemas

Blindly adding indexes to every column wastes disk space and slows down write operations. Analyzing query access logs revealed that 90% of requests filtered on compound fields, allowing us to build surgical compound and partial indexes.

TAKEAWAY 02

Hybrid Monolith Over Premature Microservices

Splitting everything into 8 microservices would have introduced distributed transaction pain. Keeping relational domain models in Django while isolating real-time WebSockets to Flask gave us the exact balance of velocity and scale.

TAKEAWAY 03

Redis Write-Through with Signal Purging

Achieving a 94% cache hit rate during peak traffic allowed a modest single-host server to handle 8,200 concurrent users without database CPU spikes.

TAKEAWAY 04

Pessimistic Locks for Financial Mutations

Optimistic concurrency models fail when donation traffic spikes in bursts. Row-level select_for_update() locks ensure mathematical precision on public fundraising targets.

Stack
Django 4.2, Flask, PostgreSQL 15, Redis 7, Celery, Docker Compose, Nginx
Key Architecture
Hybrid Monolith, Partial Indexes, Pessimistic Locking, Socket.IO
Repository
github.com/kulkarnishub377/Alumni-Portal-Core
Production architecture designed and deployed by Shubham Kulkarni. Alumni profiles and institutional identifying metrics have been scrubbed to protect privacy.