Distributed system Plate II

Enroll

Course registration system focused on keeping seat counts correct under concurrent enrollment. It combines a Next.js UI, NestJS API, PostgreSQL locking, background waitlist jobs, and an audit trail.

Stack
Next.js Next.js Prisma Prisma PostgreSQL PostgreSQL Redis Redis MongoDB MongoDB Turborepo Turborepo
Enroll application preview
Plate II · Application capture
Enroll system architecture diagram
Architecture Flow: PostgreSQL row-level locks serialize capacity checks while the transactional outbox worker dispatches background events.
Engineering notes

Architecture and decisions

High contention and the reality of registration morning

Registration morning is a high-pressure burst traffic event. At 8:00:00 AM, 1,000 university students submit requests within seconds for 40 seats in high-demand graduate seminars. When a physical lecture hall has 40 chairs, allocating 41 seats creates administrative disruptions and legal liability. The primary requirement of the system is absolute seat count integrity under concurrent load.

The breakdown of optimistic locking under load

I initially implemented optimistic concurrency control using Prisma versioning tags because non-blocking writes appeared cleaner in architecture diagrams. Under k6 stress testing with 500 concurrent virtual users hitting a single section, the optimistic model collapsed. Contending requests collided on version numbers and initiated immediate application-level retries. Those retry storms saturated the connection pool, pinned database CPU utilization at 100%, and caused 23 excess seat allocations because non-atomic checks slipped between read and conditional write operations.

Pessimistic row locking directly in PostgreSQL

The turning point was accepting that the database must own the invariant directly. I removed application-level optimistic retries and implemented raw PostgreSQL transactions with explicit SELECT FOR UPDATE row locks. When a student attempts to enroll, NestJS acquires an exclusive lock on that specific section record. Every other concurrent request for that section queues at the database layer until the active transaction commits. In load testing, overall write throughput decreased by 15%, while seat overcommits dropped to zero. In high-contention scenarios, pessimistic row serialization prevents the chaos of optimistic retry storms.

The dual-write trap and the transactional outbox

The system updates PostgreSQL for relational records, dispatches waitlist jobs to Redis BullMQ, writes audit trails to MongoDB, and serves read traffic through HTTP endpoints. Executing direct network calls to secondary data stores within the HTTP request lifecycle created partial failure states whenever MongoDB timed out after PostgreSQL committed. To isolate failures, we implemented the Transactional Outbox pattern: enrollment records and outbox events commit within the same ACID transaction in PostgreSQL. A dedicated background worker polls the outbox table using SELECT FOR UPDATE SKIP LOCKED, dispatches tasks to Redis, and writes records to MongoDB. If secondary infrastructure drops offline, registration continues without data loss.

Testing against real infrastructure

Mocked database tests hide race conditions. Our continuous integration pipeline uses Testcontainers to spin up real PostgreSQL and Redis instances for integration suites. Automated k6 scripts execute burst traffic profiles against the live transaction boundary to verify zero seat drift on every release.