The Motivation: From School Monolith to High-Concurrency Enterprise CBT
Earlier in this blog series, I documented the complete transformation of my 2017 legacy junior high school examination platform from Laravel 5.2 to modern Laravel 13, Filament 3, and Livewire 3 (open-sourced at mfarim/laravel-elearning).
That monolithic Laravel rebuild delivered immense value:
- Filament 3 enabled instantaneous administration CRUD.
- Livewire 3 brought reactive student exam timers and question switches without complex client-side build pipelines.
- Pest PHP provided rock-solid feature and security regression testing (including IDOR elimination in Part 10).
However, as deployment scales from a single junior high school to multi-school academic districts, centralized national certification tests, and large-scale simultaneous CBT sessions (Computer Based Tests), new architectural bottlenecks emerge:
- The "Exam Rush Hour" Concurrency Surge: When 2,000 to 10,000 candidates click "Start Exam" at precisely 08:00:00 AM, each PHP-FPM process consumes substantial resident memory (typically 40MB–80MB per worker). Spawning thousands of synchronous PHP workers saturates OS process limits and CPU context-switching overhead.
- Real-Time Proctoring at Scale: Teachers supervising 50–100 students per exam hall require continuous live telemetry (time remaining, current question, anti-cheat violations, tab changes). Handling bidirectional stateful real-time pipelines across thousands of active exam sessions demands an event-driven, memory-efficient message broker.
- Decoupled Multi-Client Ecosystem: Modern schools demand dedicated mobile applications (Android / iOS / CBT lockdown browsers) and progressive web apps consuming identical, strongly typed REST & WebSocket endpoints.
- Strict Enterprise Type Safety & Compliance:
Enterprise academic institutions require compile-time type verification across domain models, audit logs, and declarative role-based method security (
@PreAuthorize).
To address these needs, I engineered a high-performance, decoupled companion platform: Java Spring Boot + React E-Learning.
Architectural Comparison: Monolith vs. Decoupled Service
Here is how each critical layer transitions between the two architectures:
| Architectural Concern | Laravel Monolithic Edition | Java Spring Boot + React Edition |
|---|---|---|
| Language & Runtime | PHP 8.3 (Interpreted / JIT), PHP-FPM process-per-request | Java 21 LTS (HotSpot JVM + Virtual Threads / Project Loom) |
| Backend Framework | Laravel 13 Monolith | Spring Boot 3.4.3 (RESTful API micro-architecture) |
| Frontend Framework | Blade templates + Livewire 3 reactive server components | React 19 SPA + TypeScript + Vite 6 + Tailwind CSS v4 |
| Authentication | Stateful PHP session cookies + CSRF tokens | Stateless JWT Bearer tokens with Impersonation Engine |
| Authorization | Laravel Gate & Model Policies (#[Authorize]) | Spring Security 6 Filter Chain + @PreAuthorize method security |
| Database & Migrations | MySQL 8 with Laravel Artisan migrations | PostgreSQL 16 with Flyway versioned SQL migrations (V1__...) |
| Caching Layer | Redis cache via Laravel Cache facade | Redis 7 via Spring Data Redis & Lettuce connection pool |
| Real-Time Telemetry | Laravel Reverb / Echo client | Spring WebSocket STOMP message broker (/ws/cbt) + SockJS |
| Batch Processing | Laravel Queues + PhpSpreadsheet | Apache POI streaming batch reader for student & question .xlsx |
| API Documentation | Scramble / Scribe OpenAPI generator | Springdoc OpenAPI 3 + Swagger UI interactive explorer |
| Deployment Model | Single container / Linux VPS systemd unit | Multi-container Docker Compose (Nginx, React, Spring Boot, Postgres, Redis) |
System Architecture Diagram
Below is the decoupled cloud-ready architecture implemented in the Java Spring Boot + React repository:
Key Engineering Highlights & Deep Dive
1. Spring Security 6: Stateless JWT with Impersonation Engine
In Laravel, role switching or admin impersonation typically uses Auth::loginUsingId($targetUserId) backed by server sessions. In a decoupled single-page application (SPA), we require stateless JWT tokens that carry immutable claims while supporting on-demand impersonation.
Here is the production configuration from backend/src/main/java/com/elearning/config/SecurityConfig.java:
JAVA
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomUserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/v1/auth/**",
"/swagger-ui/**",
"/v3/api-docs/**",
"/ws/**"
).permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.requestMatchers("/api/v1/teacher/**").hasAnyRole("ADMIN", "TEACHER")
.requestMatchers("/api/v1/student/**").hasAnyRole("ADMIN", "STUDENT")
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
The Impersonation Engine
When an administrator needs to investigate a student's examination issue, they can issue a targeted impersonation token:
JAVA
@PostMapping("/impersonate")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<AuthResponse>> impersonateUser(
@Valid @RequestBody ImpersonateRequest request,
@AuthenticationPrincipal UserPrincipal adminPrincipal) {
User targetUser = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
// Generate valid scoped token bearing target's roles with original admin audit claim
String impersonationToken = jwtTokenProvider.generateTokenWithImpersonator(
targetUser,
adminPrincipal.getEmail()
);
return ResponseEntity.ok(ApiResponse.success(
"Impersonation active",
new AuthResponse(impersonationToken, targetUser.getRole().name())
));
}
2. High-Precision CBT Exam Engine: Anti-Cheat & Real-Time Telemetry
The exam runner in backend/.../modules/exam/service/ExamRunnerService.java enforces strict server-side rules that cannot be manipulated by client-side browser clocks:
Server-Driven Timer Calculation
JAVA
private long calculateRemainingSeconds(Examination exam, ExamAttempt attempt, LocalDateTime now) {
LocalDateTime examDeadline = attempt.getStartedAt().plusMinutes(exam.getDurationMinutes());
// Hard boundary: exam cannot exceed global scheduled cutoff time
if (exam.getEndAt() != null && exam.getEndAt().isBefore(examDeadline)) {
examDeadline = exam.getEndAt();
}
return Math.max(0, Duration.between(now, examDeadline).getSeconds());
}
Anti-Cheat Proctoring with WebSocket STOMP
In frontend/src/pages/exams/ExamRunner.tsx, the React runner locks down the candidate's browser window:
- Disables right-click context menus (
e.preventDefault()). - Monitors visibility state changes via the HTML5
visibilitychangeAPI. - Increments security violation counters on the server. If violations reach 5, the exam auto-finalizes immediately.
TYPESCRIPT
// React 19 Frontend: Tab switch & minimization telemetry push
useEffect(() => {
const handleVisibilityChange = () => {
if (document.hidden && attemptIdRef.current && !submittedResult) {
api.post(`/exams/attempts/${attemptIdRef.current}/violation`, {
reason: 'TAB_SWITCH_OR_MINIMIZED',
}).then((res) => {
const count = res.data.data.violations;
setViolations(count);
if (count >= 5) {
alert('Security limit reached: 5 violations detected. Auto-submitting exam.');
handleSubmitExam();
}
});
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
}, [submittedResult]);
Simultaneously, the Spring backend broadcasts telemetry packets to all connected teacher supervisor screens:
JAVA
realtimeService.broadcastExamEvent(exam.getId(), "STUDENT_VIOLATION", Map.of(
"attemptId", attempt.getId(),
"studentId", student.getId(),
"studentName", student.getUser().getName(),
"violations", attempt.getViolations(),
"timestamp", LocalDateTime.now()
));
The teacher's proctoring monitor dashboard (ExamMonitor.tsx) receives these STOMP messages in sub-millisecond latency, rendering live progress bars and alert badges without polling the server.
3. Apache POI: Memory-Safe Streaming Batch Processing
Importing large Excel spreadsheets (e.g. 5,000 enrolled students or 500 complex exam questions) often crashes monolithic PHP workers due to memory_limit exhaustion when parsing XML trees.
In Spring Boot, we leverage Apache POI with row-level validation:
JAVA
@Transactional
public BatchImportResult importStudentsFromExcel(MultipartFile file, Long classroomId) throws IOException {
Classroom classroom = classroomRepository.findById(classroomId)
.orElseThrow(() -> new ResourceNotFoundException("Classroom not found"));
int successCount = 0;
List<String> errors = new ArrayList<>();
try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) {
Sheet sheet = workbook.getSheetAt(0);
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) continue;
try {
String name = getCellValue(row.getCell(0));
String email = getCellValue(row.getCell(1));
String nis = getCellValue(row.getCell(2));
provisionStudentAccount(name, email, nis, classroom);
successCount++;
} catch (Exception e) {
errors.add("Row " + (i + 1) + ": " + e.getMessage());
}
}
}
return new BatchImportResult(successCount, errors);
}
4. Database Schema: PostgreSQL & Flyway Versioning
Unlike Laravel migrations which execute PHP code, Java enterprise backends prefer immutable, declarative SQL scripts managed by Flyway.
In backend/src/main/resources/db/migration/V1__initial_schema.sql:
SQL
CREATE TABLE examinations (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
subject_id BIGINT NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,
teacher_id BIGINT NOT NULL REFERENCES teachers(id) ON DELETE CASCADE,
classroom_id BIGINT NOT NULL REFERENCES classrooms(id) ON DELETE CASCADE,
duration_minutes INT NOT NULL DEFAULT 60,
passing_score INT NOT NULL DEFAULT 75,
randomize_questions BOOLEAN DEFAULT FALSE,
randomize_options BOOLEAN DEFAULT FALSE,
allow_review BOOLEAN DEFAULT TRUE,
start_at TIMESTAMP NOT NULL,
end_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE exam_attempts (
id BIGSERIAL PRIMARY KEY,
examination_id BIGINT NOT NULL REFERENCES examinations(id) ON DELETE CASCADE,
student_id BIGINT NOT NULL REFERENCES students(id) ON DELETE CASCADE,
attempt_number INT NOT NULL DEFAULT 1,
status VARCHAR(30) NOT NULL DEFAULT 'in_progress',
score INT,
passed BOOLEAN,
violations INT DEFAULT 0,
started_at TIMESTAMP NOT NULL,
finished_at TIMESTAMP,
CONSTRAINT uk_exam_student_attempt UNIQUE (examination_id, student_id, attempt_number)
);
The unique constraint uk_exam_student_attempt prevents race conditions where a candidate might double-submit or spawn parallel attempt sessions.
Frontend Architecture: React 19, Vite & Tailwind CSS v4
The frontend application (frontend/) is engineered for speed, clean UX, and complete mobile responsiveness:
- State Management: Zustand stores manage JWT persistence and user context across browser tabs without prop-drilling.
- Dynamic Mobile Drawer: On student mobile screens, navigation and exam details collapse into an accessible sliding drawer.
- CBT Question Palette: Fast keyboard and touch navigation between 100+ exam questions with visual state indicators:
- 🟢 Green: Answered and synchronized to server
- ⚪ Gray: Unanswered question
- 🟡 Yellow: Active question under review
- Exam Hall Ticket Generator: Print-ready candidate cards with auto-rendered SVG barcodes for official examination venue check-in.
When to Use Laravel Monolith vs. Java Spring Boot + React
Choosing the right architectural stack depends on operational constraints:
| Scenario / Requirement | Recommended Stack | Rationale |
|---|---|---|
| Single School / Small Academy (< 1,000 users) | Laravel 13 + Livewire + Filament | Fastest delivery time, minimal DevOps overhead, single codebase, batteries-included admin CRUD. |
| District-Wide Exams / University CBT (> 10,000 concurrent) | Java 21 Spring Boot + React 19 | JVM Virtual Threads handle mass concurrent I/O with low RAM. Independent horizontal scaling of API and SPA. |
| Native Mobile App Ecosystem (iOS/Android/Desktop) | Java Spring Boot API + React | Single backend API serves web portal, native mobile apps, and third-party lockdown exam browsers. |
| Small Solo Engineering Team | Laravel 13 Monolith | Full-stack productivity without maintaining separate TypeScript DTO schemas, CORS configurations, and dual CI/CD pipelines. |
| Enterprise Compliance & Strict Auditing | Java Spring Boot | Strong static typing, declarative @PreAuthorize security, enterprise Redis/Kafka integration, and long-term LTS enterprise support. |
Open-Source Repositories
Both companion implementations are publicly accessible for study, adaptation, and production deployment:
- 🐘 Laravel Monolith Edition: https://github.com/mfarim/laravel-elearning
- Laravel 13, Livewire 3, Filament 3, MySQL, Pest PHP
- ☕ Java Spring Boot + React Edition: https://github.com/mfarim/spring-elearning-react
- Java 21 LTS, Spring Boot 3.4, React 19, Tailwind CSS v4, PostgreSQL 16, Redis 7, WebSocket STOMP
Summary & Next Steps
Rebuilding the Laravel E-Learning platform into Java Spring Boot and React proves that modern software engineering is never about dogmatism. It is about selecting the optimal toolchain for each operational scale. Laravel provides unmatched development velocity for rapid execution, while Spring Boot and React deliver enterprise concurrency, resilient real-time proctoring, and cloud-native decoupled scalability.
In Part 2, we will dive into the detailed migration of the database schema from Laravel Eloquent models to JPA Entities with Flyway versioning. Stay tuned!
