MUHAMMAD
FARI
MADYAN
[ Press ESC or Click to Skip ]

Java Spring Boot + React E-Learning Rebuild

1

Part 1: Introduction & Architecture: Monolith to Decoupled Enterprise

2

Part 2: Domain Model & PostgreSQL Flyway Schema Design

3

Part 3: Stateless Authentication & RBAC with Spring Security 6 & JWT

4

Part 4: CBT Exam Engine & Real-Time Telemetry with WebSocket STOMP

5

Part 5: Modern Frontend with React 19, TypeScript & Tailwind CSS v4

6

Part 6: High-Performance Batch Processing & Production Docker Deployment

This article is available in Indonesian

🇮🇩 Baca dalam Bahasa Indonesia
🇬🇧 English📚 Java Spring Boot + React E-Learning Rebuild

Rebuilding Laravel E-Learning to Java Spring Boot & React: Enterprise Architecture & Cloud-Ready Migration

A comprehensive guide on rebuilding a monolithic Laravel E-Learning & CBT platform into a high-concurrency, cloud-ready architecture using Java 21, Spring Boot 3.4, React 19, Tailwind CSS v4, Redis, and WebSocket STOMP proctoring.

Muhammad Fari MadyanAuthor

11 min read

·

Oct 5, 2026


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:

  1. 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.
  2. 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.
  3. 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.
  4. 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 EVOLUTION OVERVIEW │ ├──────────────────────────────────────┬──────────────────────────────────────┤ │ SOURCE: Monolithic Stack │ TARGET: Decoupled Enterprise Stack │ │ (https://github.com/mfarim/ │ (https://github.com/mfarim/ │ │ laravel-elearning) │ spring-elearning-react) │ ├──────────────────────────────────────┼──────────────────────────────────────┤ │ • Laravel 13 & PHP 8.3 │ • Java 21 LTS & Spring Boot 3.4.3 │ │ • Server-Side Blade & Livewire 3 │ • React 19 + TypeScript + Vite 6 │ │ • Filament 3 Admin Panel │ • Custom Tailwind CSS v4 Dashboards │ │ • Stateful Sessions & CSRF Cookies │ • Stateless JWT (JJWT 0.12) + RBAC │ │ • Laravel Reverb / Pusher WebSockets │ • Spring STOMP Broker (/ws/cbt) │ │ • Eloquent ORM & Laravel Migrations │ • Spring Data JPA + Hibernate + Flyway│ │ • MySQL 8 Database │ • PostgreSQL 16 + Redis 7 Cache │ │ • Maatwebsite / PhpSpreadsheet │ • Apache POI Streaming Batch XLSX │ └──────────────────────────────────────┴──────────────────────────────────────┘

Architectural Comparison: Monolith vs. Decoupled Service

Here is how each critical layer transitions between the two architectures:

Architectural ConcernLaravel Monolithic EditionJava Spring Boot + React Edition
Language & RuntimePHP 8.3 (Interpreted / JIT), PHP-FPM process-per-requestJava 21 LTS (HotSpot JVM + Virtual Threads / Project Loom)
Backend FrameworkLaravel 13 MonolithSpring Boot 3.4.3 (RESTful API micro-architecture)
Frontend FrameworkBlade templates + Livewire 3 reactive server componentsReact 19 SPA + TypeScript + Vite 6 + Tailwind CSS v4
AuthenticationStateful PHP session cookies + CSRF tokensStateless JWT Bearer tokens with Impersonation Engine
AuthorizationLaravel Gate & Model Policies (#[Authorize])Spring Security 6 Filter Chain + @PreAuthorize method security
Database & MigrationsMySQL 8 with Laravel Artisan migrationsPostgreSQL 16 with Flyway versioned SQL migrations (V1__...)
Caching LayerRedis cache via Laravel Cache facadeRedis 7 via Spring Data Redis & Lettuce connection pool
Real-Time TelemetryLaravel Reverb / Echo clientSpring WebSocket STOMP message broker (/ws/cbt) + SockJS
Batch ProcessingLaravel Queues + PhpSpreadsheetApache POI streaming batch reader for student & question .xlsx
API DocumentationScramble / Scribe OpenAPI generatorSpringdoc OpenAPI 3 + Swagger UI interactive explorer
Deployment ModelSingle container / Linux VPS systemd unitMulti-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:

┌───────────────────────────────┐ │ CLIENT LAYER (SPA) │ │ React 19 + Tailwind CSS v4 │ └───────────────┬───────────────┘ │ HTTPS (REST) / WSS (WebSocket) │ ▼ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │ SPRING BOOT 3.4 BACKEND │ │ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ SECURITY & GATEWAY LAYER │ │ │ │ • JwtAuthenticationFilter (Stateless Bearer Validation) │ │ │ │ • DaoAuthenticationProvider + BCrypt Password Encoder │ │ │ │ • Method-level RBAC: @PreAuthorize("hasRole('ADMIN') or hasRole('TEACHER')") │ │ │ │ • One-Click Account Impersonation (/api/v1/auth/impersonate) │ │ │ └─────────────────────────────────────────────┬─────────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────────────────┼──────────────────────────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ User & Auth │ │ Academic & │ │ CBT Exam & │ │ │ │ Controller │ │ Learning │ │ Proctoring │ │ │ │ - Students │ │ - Classroom │ │ - Runner API │ │ │ │ - Teachers │ │ - Materials │ │ - Anti-Cheat │ │ │ │ - Impersonate│ │ - Assignment │ │ - Live STOMP │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ BUSINESS LOGIC & SERVICE LAYER │ │ │ │ • StudentService (Apache POI Batch Excel Importer) │ │ │ │ • ExamRunnerService (Auto-Scoring, Essay Queue, Synchronized Remaining Seconds) │ │ │ │ • ExamRealtimeService (SimpMessagingTemplate push to /topic/exams/{id}/telemetry) │ │ │ └─────────────────────────────────────────────┬─────────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────────────────┴──────────────────────────────────────┐ │ │ ▼ ▼ │ │ ┌──────────────────────────────┐ ┌─────────────────┐ │ │ │ PERSISTENCE LAYER │ │ CACHE & BROKER │ │ │ │ Spring Data JPA + Hibernate │ │ Redis 7 Cache │ │ │ │ Hikari Connection Pool │ │ STOMP /ws/cbt │ │ │ └──────────────┬───────────────┘ └─────────┬───────┘ │ └─────────────────┼─────────────────────────────────────────────────────────────────────┼─────────┘ │ │ ▼ ▼ ┌────────────────────────┐ ┌────────────────────────┐ │ PostgreSQL 16 │ │ Teacher Proctoring │ │ Flyway SQL Migrations │ │ Live Telemetry Screen │ └────────────────────────┘ └────────────────────────┘

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 visibilitychange API.
  • 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:

  1. State Management: Zustand stores manage JWT persistence and user context across browser tabs without prop-drilling.
  2. Dynamic Mobile Drawer: On student mobile screens, navigation and exam details collapse into an accessible sliding drawer.
  3. 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
  4. 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 / RequirementRecommended StackRationale
Single School / Small Academy (< 1,000 users)Laravel 13 + Livewire + FilamentFastest delivery time, minimal DevOps overhead, single codebase, batteries-included admin CRUD.
District-Wide Exams / University CBT (> 10,000 concurrent)Java 21 Spring Boot + React 19JVM 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 + ReactSingle backend API serves web portal, native mobile apps, and third-party lockdown exam browsers.
Small Solo Engineering TeamLaravel 13 MonolithFull-stack productivity without maintaining separate TypeScript DTO schemas, CORS configurations, and dual CI/CD pipelines.
Enterprise Compliance & Strict AuditingJava Spring BootStrong 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:


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!

Continue Reading

Next Article →

Fleet Management System Part 1: Introduction & System Architecture

Next article