Final Year Project: University Accommodation Allocation System

The full project, including the source code, screenshots, and the two documents referenced below, is available on GitLab: gitlab.com/Arnav-Salian/CO3201
Two documents in that repository are worth linking directly, in case you want more detail than this post covers:
Dissertation (PDF) - the full write-up, around 85 pages, covering everything from background research to final results.
Re-Evaluation Report (PDF) - a follow-up report covering a correction made to the test data after submission, discussed further below.
This post summarises what the project was, what I built, and what I took away from it, without the length or formality of the dissertation itself.
The problem
For my CO3201 Computer Science Project at the University of Leicester, I built a system to automate university accommodation allocation. At most universities, this process is still handled manually: staff work through applications by hand, matching students to rooms based on preferences and policy rules. During peak admission periods, when hundreds of applications are submitted at once, this becomes slow and error-prone, and students are often left with little visibility into the status of their application.
The aim was to replace that process with an automated, rules-based system that remains fair and auditable, rather than a black-box algorithm that can't be explained or reviewed.
What I built
The system, which I called LeicesterHalls, is a full-stack, cloud-hosted web application consisting of two separate React applications and a serverless backend running entirely on AWS.
Client application: the student-facing side. Students can register, browse accommodation, submit an application with ranked preferences, track their application status, accept or decline room offers, make payments through PayPal, and view their contract.
Admin application: the interface for accommodation staff. Admins can manage room inventory, trigger allocation runs, override allocation decisions, handle room change and termination requests, and monitor occupancy and revenue through dashboards.
Both applications call the same backend API, but each has its own authentication and authorization setup, so they had to be designed and secured largely independently despite sharing infrastructure.
The stack, briefly:
Frontend: React.js with Vite, MUI Joy UI, Axios, AWS Amplify for authentication, and PayPal's Checkout SDK for payments.
Backend: Node.js Lambda functions behind a single API Gateway REST API, with shared dependencies packaged as Lambda Layers.
Data: DynamoDB with on-demand capacity, so the database scales without needing traffic to be estimated in advance.
Auth: Two separate Cognito User Pools, one for students and one for admins.
Infrastructure: A custom VPC, EC2 instances in Auto Scaling Groups behind Application Load Balancers, Route 53 for DNS, and ACM certificates so both applications run over HTTPS.
How traffic flows through the system
This was the part of the project that took the most iteration, since it involved getting a number of AWS services to work together correctly. The diagram below is the architecture diagram from the dissertation:
Loading the page. A request to the client app or the admin app reaches Route 53 first, which resolves the domain and routes it to the correct Application Load Balancer, since each application has its own. The ALB terminates TLS using the relevant ACM certificate, then forwards the request over HTTP to a healthy EC2 instance in its Auto Scaling Group. Both Auto Scaling Groups span two Availability Zones, eu-west-2a and eu-west-2b, so the system continues to operate if one zone becomes unavailable. Each EC2 instance runs nginx and serves the static React build, which is pulled from an S3 bucket by a user data script when the instance launches.
Making API calls. Once the application has loaded, any user action, whether that's logging in, submitting an application, or loading a dashboard, is sent to API Gateway. Protected routes require a JWT access token in the request header, which API Gateway validates against one of three Cognito authorizers: one for students, one for admins, and a shared authorizer for endpoints both roles can access. Requests with an invalid or missing token are rejected before they reach any backend logic.
Processing the request. Once a request passes the authorizer, API Gateway forwards it to the relevant Lambda function using proxy integration. That function executes the business logic, whether that's reading room data, running the allocation algorithm, or writing a new application, and interacts with DynamoDB using IAM permissions scoped to exactly what it needs. The response is returned through API Gateway to the browser, and each function writes its logs to its own CloudWatch log group, which proved useful throughout development when debugging permission issues.
Authentication follows a similar but separate flow. Registration triggers a PreSignUp Lambda function that checks the student ID and date of birth against a Students table before Cognito creates the account, which prevents unauthorised sign-ups. After email verification, a PostConfirm Lambda writes the corresponding user record to DynamoDB. On login, Cognito returns ID, access, and refresh tokens, which are stored client-side and used to authenticate subsequent requests.
The allocation algorithm
This was the core of the project; everything else exists to support it. When an admin triggers an allocation run for an academic year, a Lambda function retrieves every submitted application and every available room, then runs a policy-constrained greedy assignment algorithm.
Students are processed in priority order: accessibility requirements first, then course type, then submission timestamp. For each student, the algorithm attempts to match them to their highest-ranked accommodation and room choice, out of up to four ranked preferences, provided it satisfies hard constraints such as gender policy, budget, and accessibility requirements. If none of their ranked choices are available, a fallback allocation is made based on the student's stated secondary priorities. Students who cannot be matched under any condition are recorded as unallocated for manual review. Every allocation, offer, occupancy update, and notification is written atomically using DynamoDB transactions, so no partial updates occur.
I chose a greedy, priority-respecting approach over something like Gale-Shapley stable matching, because stable matching doesn't handle hard eligibility constraints well: a room that fails a student's accessibility requirement isn't a valid match regardless of ranking. The definition of fairness I used instead was more direct: no student should be able to point to a room they preferred and were eligible for that was given to a lower-priority student.
Testing it
To evaluate the allocation logic, I built a synthetic dataset of 51 student applications against 234 available rooms for the 2026/2027 academic year, written manually rather than generated randomly, to keep the mix of preferences, budgets, and accessibility needs realistic. Running this through the system produced:
94.1% allocation rate (48 of 51 students successfully allocated)
87.5% of allocated students received one of their ranked preferences
54.2% received their first choice
Running the same dataset through the allocation process twice produced identical results, which mattered given the intent of the system: consistency, not variability, is a requirement for a fairness-sensitive process.
I also stress-tested the infrastructure directly. After SSHing into the EC2 instances, I installed the stress tool and ran stress --cpu 2 to push CPU utilisation to 100%. Once utilisation crossed the 60% threshold configured on the scaling policy, both Auto Scaling Groups provisioned an additional instance each, and the load balancer correctly distributed traffic between the original and new instances.
Finding a bug after submission
After submitting the dissertation, I reviewed the test data again and found an error in the synthetic room dataset that had affected the allocation results reported above.
Each room record includes a RoomInfoID field linking it to an entry in its accommodation's list of room types, such as Single Ensuite, Studio Room, or Adapted Single Ensuite. In the seed data I had built, every room in every flat had been assigned the first RoomInfoID in that list rather than the one matching its actual room type. Because Adapted Single Ensuite happened to be listed first for several accommodations, most of the 234 rooms in the dataset were effectively mislabelled as accessible rooms, regardless of what they actually were.
This produced two failure modes. Because the algorithm correctly restricts accessible rooms to students who have declared an accessibility requirement, the majority of the room pool was incorrectly excluded from non-accessible students, forcing fallback allocations. Separately, students who had ranked a specific room type, a Studio Room, for example, could never be matched to it, because the system had recorded that room under the wrong type. Neither issue was a fault in the algorithm; it behaved exactly as designed against incorrect input data.
I corrected the room type labels in a new dataset (Rooms_Patched.json), without modifying the algorithm or any backend code, and reran the same 51 applications against the same 234 rooms:
| Metric | Original dataset | Corrected dataset |
|---|---|---|
| Successfully allocated | 48 / 51 (94.1%) | 51 / 51 (100%) |
| Unallocated | 3 (5.9%) | 0 (0%) |
| First choice matches | 26 (54.2%) | 45 (88.2%) |
| Second choice matches | 7 (14.6%) | 6 (11.8%) |
| Fallback allocations | 6 (12.5%) | 0 (0%) |
| Preference match rate | 87.5% | 100% |
With correct room type data, every student was successfully allocated, no fallback allocations were required, and 88.2% received their first choice. Re-running the corrected dataset produced identical results again, consistent with the determinism claimed in the dissertation.
I wrote this up as a formal re-evaluation report to make sure the correction and its impact on the results were clearly documented. It's also a useful reminder that a single incorrect field in seed data can affect an entire set of results without being obvious from the output alone.
What I would do differently
A few limitations are worth acknowledging. 51 applications is enough to demonstrate that the logic functions correctly, but it is far smaller than a real university deployment; the University of Leicester alone manages around 4,600 rooms, so a more representative test would need a substantially larger dataset, ideally generated programmatically rather than by hand. Contracts and invoices are currently generated client-side as PDFs on demand, which reduces storage costs but means no server-side copy exists unless the student downloads it. The PayPal client ID and secret are also currently stored as Lambda environment variables rather than in a dedicated secrets manager, which would be the more appropriate approach for a production deployment.
Given the chance to start over, I would build automated test data generation in from the outset rather than adding it later, which would likely have caught the RoomInfoID error before it reached a submitted result. I would also spend more time analysing DynamoDB access patterns before finalising the schema; the denormalised structure was adequate for a project of this scope but became harder to manage as more tables and relationships were introduced.
Beyond the technical work, this project extended further than anything I had built in previous coursework. I had some prior experience with React and a general familiarity with AWS, but had not previously combined Cognito, Amplify, Lambda, DynamoDB, PayPal integration, and auto-scaling infrastructure into a single system. Most of what I learned came from debugging integration issues rather than from building new features; getting IAM roles and Cognito authorizers configured consistently across different endpoints took considerably longer than expected, and was also where I gained the most practical understanding of how cloud security is actually implemented.
Conclusion
The project achieved what it set out to do. It is a working, deployed system that takes students through the full accommodation process, from application through to allocation, contract signing, and payment, running on cloud infrastructure that scales as intended. The version submitted for assessment reported a 94.1% allocation rate; after identifying and correcting the room data error, the corrected figures show a 100% allocation rate with 88.2% of students receiving their first choice, a more accurate reflection of the algorithm's actual performance. Infrastructure testing also confirmed that the scaling configuration behaves correctly under load.
A few things would need to happen before this system could realistically go into production: testing at a larger scale with a properly validated dataset, and possibly letting students rank more than four preferences to improve preference match rates in a larger deployment. As a final year project, however, it met its original aims: a system that is fair, scalable, and usable by both students and staff, rather than a proof of concept alone.
The full project is available on GitLab: gitlab.com/Arnav-Salian/CO3201





