Back to blog
Building a Production University Management System with Laravel
How we designed and built a three-portal university management system serving admin, student, and public-facing needs — covering architecture, payroll engine, multi-gateway payments, SMS integrations, and double-entry accounting on Laravel 9.
15 min read·Talha Bilal
Share:

Introduction
Running a university requires coordinating admissions, examinations, fee collection, payroll for hundreds of staff, library management, hostel allocation, transport scheduling, and financial accounting — all while maintaining a public-facing website and a self-service student portal. Spreadsheets and disjointed software create data silos, manual errors, and administrative overhead.
This case study covers the design and implementation of a three-portal university management system built for TAHSEEN AHMAD CHEEMA INSTITUTE, a degree-awarding institution in Bahawalpur, Pakistan. The system consolidates 14+ operational domains into a single Laravel 9 application with role-based access for admin staff, students, and the general public.
Problem & Business Context
The institute operated with a mix of manual processes and disconnected tools. Admissions were handled through paper forms and spreadsheets. Fee tracking required cross-referencing multiple records. Payroll calculations were done manually, prone to arithmetic errors. The public website was static, unable to display real-time updates like merit lists or event calendars.
The core problems to solve:
- Data fragmentation — student records, fees, attendance, and grades lived in separate systems with no single source of truth
- Manual payroll — calculating allowances, deductions, taxes, and loan installments for each employee by hand
- Payment friction — students could only pay fees via bank deposits, requiring manual reconciliation
- Communication gaps — no systematic way to send fee reminders, notices, or alerts to students and staff
- Compliance — need for proper double-entry accounting and audit trails for financial transactions
- Public presence — the website needed dynamic content management and online application capabilities
Note: All claims in this article are based on the actual codebase (178 database migrations, ~340 PHP files, 6 payment gateway integrations, 6 SMS provider implementations).
Goals
The project had several high-level objectives:
- Single platform — one codebase serving admin dashboard, student portal, and public website
- Role-based access — granular permissions so staff only access what they need
- Automated payroll — engine that calculates net salary from base + allowances - deductions - tax - loans
- Online payments — support multiple payment gateways so students can pay from anywhere
- Multi-channel communication — email and SMS notifications with provider-agnostic architecture
- Double-entry accounting — every financial transaction creates proper journal entries
- Self-service student portal — attendance, fees, assignments, library, leave applications
- CMS-powered website — non-technical staff should update sliders, news, events, and faculty profiles
Solution Overview
The solution is a monolithic Laravel 9 application with three distinct access points:
| Portal | Auth Guard | Audience | Primary Actions |
|---|---|---|---|
Admin Dashboard (/admin/*) | web (session) | Institute staff | Manage all modules, generate reports, process payroll |
Student Portal (/student/*) | student (session) | Enrolled students | View routine, pay fees, submit assignments, apply for leave |
Public Website (/*) | None | Visitors, applicants | Browse courses, apply online, view merit lists |
All three share the same database, models, and service layer but expose different views and capabilities based on authentication guard and permissions.
Architecture
The system follows Laravel's MVC pattern with an added service layer for complex business logic.
Request Lifecycle
TODO: Add a Mermaid flowchart illustrating the request lifecycle: HTTP request → Middleware stack (XSS, license, auth, localization) → Route matching → Controller → Service layer → Eloquent → Blade view / JSON response.
Module Organization
Controllers are organized by user role rather than by feature:
text
1app/Http/Controllers/2├── Admin/ # 133 controllers — one per admin module3├── Admin/Web/ # 29 controllers — CMS admin panels4├── Auth/ # 5 controllers — login, registration, password reset5├── Payment/ # 6 controllers — PayPal, Stripe, Razorpay, Paystack, Flutterwave, Skrill6├── Student/ # 14 controllers — student self-service7├── Teacher/ # 1 controller — result status8└── Web/ # 24 controllers — public websiteModels reside in
app/Models/ (~101 models) and app/Models/Web/ (~28 CMS models), with one legacy exception: App\User (not in the Models namespace) due to auth scaffolding conventions.Service Layer
Business logic that doesn't belong in controllers lives in dedicated service classes:
TODO: Add a Mermaid class diagram showing the SMS service factory pattern with
SMSServiceInterface, SMSServiceFactory, and the 6 provider implementations.The SMS module exemplifies the factory pattern:
php
1// app/Services/SMS/SMSServiceFactory.php2public static function create($gateway)3{4 return match ($gateway) {5 'twilio' => new TwilioService(),6 'vonage' => new VonageService(),7 'textlocal' => new TextLocalService(),8 'clickatell' => new ClickatellService(),9 'africastalking' => new AfricasTalkingService(),10 'smscountry' => new SMSCountryService(),11 default => null,12 };13}Each provider implements
SMSServiceInterface with a sendSms($to, $message) contract. The SMSSender trait on controllers resolves the active gateway from the SMS_GATEWAY environment variable and dispatches a queued job (SMSSenderJob), keeping the HTTP response fast.Frontend Architecture
The admin dashboard uses Bootstrap 4 with the PCoded theme. The public website is a custom Bootstrap 4 theme with Slick slider, Magnific Popup, and animation libraries. Vue 2 is installed but used minimally — one example component exists. The application is predominantly server-rendered Blade templates with jQuery-enhanced interactivity.
TODO: Add a screenshot showing the admin dashboard with analytics charts (line, bar, pie) and summary counters.
Asset compilation uses Laravel Mix 4:
js
1// webpack.mix.js2const mix = require('laravel-mix');3
4mix.js('resources/js/app.js', 'public/js')5 .sass('resources/sass/app.scss', 'public/css');6
7mix.styles([8 'public/web/css/bootstrap.css',9 'public/web/css/style.css',10 'public/web/css/responsive.css'11], 'public/css/web-all.css');12
13mix.scripts([14 'public/web/js/popper.min.js',15 'public/web/js/bootstrap.min.js',16 'public/web/js/appear.js',17 'public/web/js/isotope.js',18 'public/web/js/mixitup.js',19 'public/web/js/script.js'20], 'public/js/web-all.js');Technical Decisions
Why Laravel 9?
The choice was pragmatic. The institute needed a mature, well-documented framework with built-in authentication, ORM, queue system, mail, and scheduling — all of which Laravel provides out of the box. PHP 8.0+ support meant we could use named arguments, match expressions, and union types for cleaner code.
Why Monolithic Instead of Microservices?
A monolithic architecture is appropriate here because:
- The team size and update frequency don't justify distributed system overhead
- All domains share the same database — splitting would introduce complex join queries across service boundaries
- Deployment is simpler — one artifact to build, one server to maintain
- Laravel's service layer pattern keeps modules decoupled within the monolith
Database-Driven Timezone
Rather than hardcoding the timezone in
.env, the application reads it from the settings table at boot time:php
1// app/Providers/AppServiceProvider.php2Config::set('app.timezone', $setting->time_zone);This allows non-technical staff to change the timezone through the admin panel without touching configuration files or redeploying.
Why Not Vue.js for the Frontend?
The existing team was comfortable with jQuery and Blade. Introducing a full SPA framework would have increased complexity, required API endpoints for every interaction, and slowed development. Vue 2 was included for future migration but wasn't the primary rendering strategy. The admin dashboard has ~156 Blade subdirectories — migrating to a SPA would be a separate, dedicated effort.
Queue Driver Choice
Using the
database queue driver instead of Redis was deliberate. The institute may not have Redis infrastructure available. The database queue requires no additional services — it writes jobs to a jobs table and processes them via the Laravel worker. For the volume of SMS and notification jobs this system generates, database queues are sufficient.Key Features
Payroll Engine
The payroll module is one of the most complex subsystems. It comprises six service classes:
| Service | Responsibility |
|---|---|
PayrollEngine | Orchestrates the full calculation pipeline |
SalaryCalculationService | Computes base salary from employee record |
AllowanceCalculationService | Computes allowances by configured type |
DeductionCalculationService | Computes standard deductions |
TaxCalculationService | Applies tax rules per employee config |
LoanService | Deducts monthly loan installments |
The calculation follows this flow:
TODO: Add a Mermaid flowchart showing the payroll calculation pipeline: Employee data → Base salary → Allowances → Deductions → Tax → Loan installments → Net salary → Payroll record.
A payroll lifecycle moves through four states: generated → approved → locked → paid. Batch operations allow processing dozens of employees simultaneously.
Multi-Gateway Payment System
Students can pay fees through six different online payment gateways:
- PayPal (
srmklive/paypal) - Stripe (
stripe/stripe-php) - Razorpay (
razorpay/razorpay) - Paystack (
unicodeveloper/laravel-paystack) - Flutterwave (
kingflamez/laravelrave) - Skrill (
obydul/laraskrill)
Each gateway has a dedicated controller and service class. The active gateway is configured via the
PAYMENT_GATEWAY environment variable. The system also supports manual payment recording for cash and bank deposits.Double-Entry Accounting
Every financial event — fee payment, expense recording, income entry — creates proper journal entries with debits and credits. The accounting subsystem includes:
- Chart of accounts with hierarchical structure
- Journal entry lines with account, debit, and credit columns
- Accounting periods (fiscal years)
- Financial reports: trial balance, balance sheet, income statement, cash flow statement, general ledger
- Integration hooks that automatically post fee collections to the general ledger
Role-Based Access Control
Spatie Laravel Permission powers a granular RBAC system:
php
1// routes/web.php — permission-checked route example2Route::get('fees-student', [FeesStudentController::class, 'index'])3 ->name('admin.fees-student.index')4 ->middleware('permission:fees-student-list|fees-student-view');Blade templates conditionally render UI elements based on permissions:
blade
1@can('fees-student-due')2<li><a href="{{ route('admin.fees-student.index') }}">3 <i class="fas fa-money-bill-wave"></i> Due Fees4</a></li>5@endcanScheduled Tasks
Four artisan commands run on a schedule without human intervention:
| Command | Frequency | Purpose |
|---|---|---|
fees:reminder | Daily | Sends fee reminders to students with outstanding balances |
notice:send | Daily at 01:01 | Publishes notices that were scheduled for future dates |
content:send | Daily at 02:01 | Publishes scheduled learning content |
leave:update-status | Daily at 00:05 | Auto-approves or expires leave requests based on rules |
Database Design
The database has 178 migration files creating ~110+ tables. Key design patterns:
Polymorphic Relations
Several features use polymorphic relationships to stay flexible:
documents+docables— any model (student, staff) can have attached documentsnotes+noteables— notes attachable to users, students, etc.notices+noticeables— notices targetable to specific groupsstatus_type_student— students can have multiple status types (active, graduated, suspended)
Pivot-Heavy Architecture
Academic structure relies on many-to-many pivot tables:
sql
1program_subject, batch_program, program_semester,2program_class_room, program_session, program_semester_sections,3fees_master_student_enroll, exam_routine_user, exam_routine_room,4transport_route_transport_vehicleMigration Strategy
Migrations are timestamped and cover incremental schema changes over two years (mid-2021 through mid-2026). Late-stage additions like payroll enhancements and accounting modules were added without breaking existing data — a testament to Laravel's migration system and careful schema design.
TODO: Add a Mermaid ER diagram showing core entities: students, enrollments, programs, semesters, fees, transactions, and their relationships.
Implementation Details
Global Helpers
Five auto-loaded helper functions provide consistent formatting across the codebase:
php
1// app/helpers.php2numberToWords($amount, 'Rupees'); // "One Thousand Five Hundred Rupees"3numberToWordsPKR(1500); // "One Thousand Five Hundred Rupees Only"4normalize_isbn('0-306-40615-2'); // Returns normalized ISBN with validity flag5is_valid_isbn('0306406152'); // trueThese are loaded via
composer.json's files autoload key and are available globally without facade imports.Reusable Traits
Six traits encapsulate cross-cutting concerns:
| Trait | Used By | Purpose |
|---|---|---|
FileUploader | 20+ controllers | Consistent file upload, validation, and storage |
SMSSender | Notification controllers | Queued SMS dispatch via active provider |
FeesStudent | Fee controllers | Shared fee computation logic |
DoubleEntryAccounting | Fee + Accounting controllers | Automatic journal entry creation |
SummernoteEditor | CMS controllers | Rich text media handling |
EnvironmentVariable | Settings controllers | Runtime .env updates via admin panel |
Queue Jobs
Five job classes handle asynchronous work:
SMSSenderJob— sends SMS via configured providerSMSFeesReminderJob— sends bulk fee reminder SMSNotifyStaffJob/NotifyStudentJob— database notifications for in-app alertsLeaveStatusUpdateJob— processes leave status transitions
Note: All jobs use thedatabasequeue driver, meaning no Redis or Beanstalkd is required.
Challenges & Tradeoffs
Monolith vs. Microservices
The monolith works well for this scale but has growing pains. The single
routes/web.php file exceeds 1,000 lines. All controllers, models, and views live in a single application. If the institute grows to multiple campuses with independent databases, a service-oriented split would become necessary.No Form Request Classes
Validation is handled inline in controllers using
$request->validate(). While functional, this means validation rules are scattered across controller methods rather than centralized. For a project of this size, extracting Form Request classes would improve maintainability.Minimal API Surface
Only 3 API endpoints exist (
/api/departments/by-faculty, /api/programs/by-department, /api/user). This is sufficient for the current AJAX-driven admin panel but would need significant expansion to support mobile apps or third-party integrations.Vue.js Not Fully Utilized
Vue 2 is installed but the team chose jQuery for most interactivity. This is a pragmatic decision that prioritized shipping over architectural purity, but it means the frontend lacks component reusability and state management that Vue would provide.
No Automated Tests
The
tests/ directory contains only two stub tests with no assertions. This is a significant gap — payroll calculations, fee distributions, and accounting entries are prime candidates for unit tests. Adding test coverage would increase confidence when modifying core business logic.Security Considerations
- XSS protection — Custom
XSSProtectionmiddleware sanitizes all input across nearly every route - CSRF — Laravel's built-in CSRF protection is active on all web routes
- License verification —
LicenseVerificationmiddleware checks purchase code on each request - Ban detection —
IsUserBannedmiddleware blocks flagged users at the session level - Authentication guards — Separate guards for admin (
web) and student (student) prevent privilege escalation - Password resets — Separate password reset tables and brokers for admin vs. student users
- Input trimming —
TrimStringsandConvertEmptyStringsToNullmiddleware normalize input - No secrets in code — All credentials are environment variables (DB, mail, SMS, payment gateways)
Warning: No Form Request classes mean validation logic is less discoverable than it could be. Adding centralized request validation is a recommended improvement.
Developer Experience
Convention Over Configuration
The project follows Laravel conventions closely:
- Controllers grouped by user role (
Admin/,Student/,Web/) - Models in
app/Models/(with one legacy exception) - Views organized by portal (
resources/views/admin/,student/,web/) - Routes centralized in
routes/web.php
Consistent Controller Pattern
Every controller follows the same structure:
php
1class SomeController extends Controller2{3 public function __construct()4 {5 $this->title = trans_choice('module_name', 1);6 $this->route = 'admin.some-module';7 $this->view = 'admin.some-module';8 }9 10 public function index()11 {12 $data['title'] = $this->title;13 $data['route'] = $this->route;14 $data['view'] = $this->view;15 // ... query and return view16 }17}This consistency makes it easy for any Laravel developer to navigate the codebase after understanding one controller.
Tooling
- Laravel Pint — PSR-12 code style fixing (
./vendor/bin/pint) - PHPUnit — Test runner (currently 2 stub tests)
- Laravel Mix — Asset compilation with watch mode for development
What I'd Do Differently
- Add Form Request classes — centralize validation for the 200+ controller methods
- Write tests for the payroll engine — the allowance/tax/deduction calculations are ideal for data-driven tests
- Split
routes/web.php— breaking the 1,000+ line route file into feature-specific files would improve navigability - Replace jQuery with a modern vanilla JS or Alpine.js approach — reduces dependency weight and improves maintainability
- Add API endpoints — even if no mobile app exists today, versioned API endpoints for core CRUD operations would future-proof the system
Lessons Learned
-
Monoliths are fine for this scale. Not every project needs microservices. A well-organized Laravel monolith with a service layer can handle 14+ domains without the operational complexity of distributed systems.
-
Service layer patterns pay off. The SMS factory pattern and payroll engine services kept business logic clean and testable (conceptually), even though the test suite is empty.
-
Database design is the foundation. The 110+ table schema with polymorphic relations and well-named pivot tables made it possible to add features incrementally over two years without migrations conflicting.
-
Pragmatic frontend choices matter. Choosing Blade + jQuery over a full SPA meant the project shipped faster. Vue was available for gradual adoption when the team was ready.
-
Environment-driven configuration is essential. With 6 SMS providers, 6 payment gateways, and dynamic timezone settings, having all configuration in
.envand the database (not hardcoded) made deployment and maintenance straightforward.
Key Takeaways
-
One codebase, three portals — A single Laravel application serves admin staff, students, and the public through auth-guarded separation, sharing models and business logic across all three.
-
Service layer for complex domains — The payroll engine (6 classes), SMS system (factory pattern with 6 providers), and payment gateways (6 integrations) demonstrate how to keep business logic decoupled from controllers.
-
Production-ready but room to grow — The system handles real academic operations with RBAC, double-entry accounting, queued jobs, and multi-gateway payments. Adding test coverage and API endpoints would be the next priority areas.
Have questions about this architecture or want to discuss building something similar? Reach out through the contact form.

