MUHAMMAD
FARI
MADYAN
[ Press ESC or Click to Skip ]

Rebuild Java Spring Boot + React E-Learning

1

Part 1: Pengenalan & Arsitektur: Dari Monolit ke Enterprise Terpisah

2

Part 2: Desain Model Domain & Skema PostgreSQL Flyway

3

Part 3: Autentikasi Stateless & RBAC dengan Spring Security 6 & JWT

4

Part 4: Engine Ujian CBT & Telemetri Real-Time dengan WebSocket STOMP

5

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

6

Part 6: Batch Processing Performa Tinggi & Deployment Docker Produksi

Artikel ini tersedia dalam Bahasa Inggris

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

Membangun Ulang Laravel E-Learning ke Java Spring Boot & React: Arsitektur Enterprise & Migrasi Cloud-Ready

Panduan lengkap membangun ulang platform monolitik Laravel E-Learning & CBT ke arsitektur enterprise high-concurrency cloud-ready dengan Java 21, Spring Boot 3.4, React 19, Tailwind CSS v4, Redis, dan telemetri proctoring WebSocket STOMP.

Muhammad Fari MadyanPenulis

11 menit baca

·

5 Oktober 2026


Latar Belakang & Motivasi: Dari Monolit Sekolah ke CBT Enterprise Concurrency Tinggi

Pada artikel sebelumnya di blog ini, saya telah mendokumentasikan transformasi menyeluruh dari aplikasi ujian online SMP tahun 2017 berbasis Laravel 5.2 menuju Laravel 13 modern dengan Filament 3 dan Livewire 3 (tersedia open-source di mfarim/laravel-elearning).

Versi monolitik Laravel tersebut memberikan produktivitas luar biasa:

  • Filament 3 memungkinkan pembuatan panel administrasi CRUD dalam hitungan jam.
  • Livewire 3 menghadirkan pengalaman ujian reaktif tanpa perlu mengelola pipeline build frontend terpisah yang rumit.
  • Pest PHP memastikan seluruh alur bisnis dan penguatan celah IDOR (Insecure Direct Object Reference) teruji secara otomatis.

Namun, ketika sistem ini mulai dipertimbangkan untuk kebutuhan yang jauh lebih besar—seperti ujian serentak antarsekolah dalam satu distrik, sertifikasi profesional berskala ribuan peserta, atau platform Computer Based Test (CBT) terpusat, muncul tantangan skalabilitas baru:

  1. Lonjakan Konkurensi Saat "Exam Rush Hour": Ketika 2.000 hingga 10.000 siswa serempak mengklik tombol "Mulai Ujian" tepat pukul 08:00:00 WIB, setiap proses worker PHP-FPM mengonsumsi memori sekitar 40MB–80MB. Membuka ribuan proses PHP secara bersamaan membebani batas memori server dan overhead pergantian context CPU.
  2. Proctoring (Pengawasan) Real-Time dalam Skala Besar: Guru pengawas di ruang kontrol memerlukan telemetri langsung (sisa waktu, progres soal, peringatan keluar aplikasi/pindah tab). Menangani ribuan koneksi real-time dua arah membutuhkan message broker yang sangat efisien dan hemat memori.
  3. Kebutuhan Ekosistem Multi-Platform: Lembaga pendidikan modern sering memerlukan aplikasi mobile khusus (Android, iOS) atau aplikasi desktop lockdown browser yang mengakses REST API dan WebSocket yang sama dengan tipe data yang ketat (strongly typed).
  4. Keamanan Tipe & Kepatuhan Enterprise: Lingkungan enterprise menuntut verifikasi tipe data saat kompilasi (compile-time check), jejak audit menyeluruh, dan kontrol otorisasi tingkat method yang deklaratif (@PreAuthorize).

Untuk menjawab kebutuhan tersebut, saya membangun edisi pendamping enterprise: Java Spring Boot + React E-Learning.

┌─────────────────────────────────────────────────────────────────────────────┐ │ RINGKASAN EVOLUSI ARSITEKTUR SISTEM │ ├──────────────────────────────────────┬──────────────────────────────────────┤ │ SUMBER: Stack Monolitik │ TARGET: Stack Enterprise Terpisah │ │ (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 │ • Dashboard Kustom Tailwind CSS v4 │ │ • Stateful Sessions & Cookie CSRF │ • Stateless JWT (JJWT 0.12) + RBAC │ │ • Laravel Reverb / Pusher WebSockets │ • Broker Spring STOMP (/ws/cbt) │ │ • Eloquent ORM & Migrasi Laravel │ • Spring Data JPA + Hibernate + Flyway│ │ • Database MySQL 8 │ • PostgreSQL 16 + Cache Redis 7 │ │ • PhpSpreadsheet Batch Import │ • Streaming Apache POI Batch XLSX │ └──────────────────────────────────────┴──────────────────────────────────────┘

Perbandingan Arsitektur: Monolit vs. Layanan Terpisah (Decoupled)

Berikut adalah perbandingan mendalam antara kedua pendekatan arsitektur:

Aspek ArsitekturEdisi Monolitik LaravelEdisi Java Spring Boot + React
Bahasa & RuntimePHP 8.3 (Interpreted / JIT), model eksekusi proses-per-request via PHP-FPMJava 21 LTS (HotSpot JVM + Virtual Threads / Project Loom)
Framework BackendLaravel 13 MonolithSpring Boot 3.4.3 (RESTful API micro-architecture)
Framework FrontendBlade templates + komponen reaktif Livewire 3React 19 SPA + TypeScript + Vite 6 + Tailwind CSS v4
AutentikasiCookie session stateful + token CSRFToken Stateless JWT Bearer dengan Fitur Impersonasi
Otorisasi & KebijakanLaravel Gate & Model Policies (#[Authorize])Spring Security 6 Filter Chain + method security @PreAuthorize
Database & MigrasiMySQL 8 dengan Laravel Artisan MigrationsPostgreSQL 16 dengan versioned SQL migrations via Flyway (V1__...)
Layer CachingCache Redis via Laravel Cache facadeCache Redis 7 via Spring Data Redis & Lettuce connection pool
Telemetri Real-TimeLaravel Reverb / Echo clientSpring WebSocket STOMP message broker (/ws/cbt) + SockJS
Batch ProcessingLaravel Queues + PhpSpreadsheetApache POI streaming batch reader untuk berkas .xlsx siswa & soal
Dokumentasi APIScramble / Scribe OpenAPI generatorSpringdoc OpenAPI 3 + Swagger UI interactive explorer
Model DeploymentContainer tunggal / VPS Linux systemd unitMulti-container Docker Compose (Nginx, React, Spring Boot, Postgres, Redis)

Diagram Arsitektur Sistem

Berikut adalah alur arsitektur cloud-ready terpisah yang diimplementasikan pada proyek Java Spring Boot + React:

┌───────────────────────────────┐ │ LAYER CLIENT (SPA) │ │ React 19 + Tailwind CSS v4 │ └───────────────┬───────────────┘ │ HTTPS (REST) / WSS (WebSocket) │ ▼ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │ SPRING BOOT 3.4 BACKEND │ │ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ LAYER KEAMANAN & GATEWAY │ │ │ │ • JwtAuthenticationFilter (Validasi Stateless Bearer Token) │ │ │ │ • DaoAuthenticationProvider + Pengacak Sandi BCrypt │ │ │ │ • RBAC Level Method: @PreAuthorize("hasRole('ADMIN') or hasRole('TEACHER')") │ │ │ │ • Impersonasi Akun Instan (/api/v1/auth/impersonate) │ │ │ └─────────────────────────────────────────────┬─────────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────────────────┼──────────────────────────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Controller │ │ Controller │ │ Controller │ │ │ │ User & Auth │ │ Akademik & │ │ CBT Exam & │ │ │ │ - Siswa │ │ Pembelajaran │ │ Proctoring │ │ │ │ - Guru │ │ - Kelas │ │ - Runner API │ │ │ │ - Impersonate│ │ - Materi │ │ - Anti-Cheat │ │ │ │ │ │ - Tugas │ │ - Live STOMP │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ LAYER LOGIKA BISNIS (SERVICES) │ │ │ │ • StudentService (Batch Importer Excel via Apache POI) │ │ │ │ • ExamRunnerService (Auto-Scoring, Antrean Esai, Perhitungan Detik Sinkron) │ │ │ │ • ExamRealtimeService (SimpMessagingTemplate push ke /topic/exams/{id}/telemetry) │ │ │ └─────────────────────────────────────────────┬─────────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────────────────┴──────────────────────────────────────┐ │ │ ▼ ▼ │ │ ┌──────────────────────────────┐ ┌─────────────────┐ │ │ │ LAYER PERSISTENSI │ │ CACHE & BROKER │ │ │ │ Spring Data JPA + Hibernate │ │ Redis 7 Cache │ │ │ │ HikariCP Connection Pool │ │ STOMP /ws/cbt │ │ │ └──────────────┬───────────────┘ └─────────┬───────┘ │ └─────────────────┼─────────────────────────────────────────────────────────────────────┼─────────┘ │ │ ▼ ▼ ┌────────────────────────┐ ┌────────────────────────┐ │ PostgreSQL 16 │ │ Layar Pengawas │ │ Migrasi SQL Flyway │ │ Telemetri Ujian Guru │ └────────────────────────┘ └────────────────────────┘

Sorotan Rekayasa Utama & Bedah Kode

1. Spring Security 6: Stateless JWT & Engine Impersonasi

Di Laravel, impersonasi atau berganti identitas pengguna umumnya dilakukan menggunakan Auth::loginUsingId($targetUserId) yang mengandalkan sesi server. Pada aplikasi terpisah (SPA), kita membutuhkan token JWT stateless yang membawa hak akses pengguna tanpa bergantung pada sesi server, sekaligus mendukung audit impersonasi.

Berikut konfigurasi keamanan produksi dari 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();
    }
}

Mekanisme Impersonasi Akun

Saat admin perlu menginvestigasi kendala pengerjaan ujian siswa secara langsung di lapangan:

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 tidak ditemukan"));
    
    // Hasilkan token berlingkup peran target yang tetap mencatat identitas admin sebagai audit
    String impersonationToken = jwtTokenProvider.generateTokenWithImpersonator(
            targetUser, 
            adminPrincipal.getEmail()
    );
    
    return ResponseEntity.ok(ApiResponse.success(
            "Impersonasi berhasil diaktifkan",
            new AuthResponse(impersonationToken, targetUser.getRole().name())
    ));
}

2. Engine Ujian CBT: Anti-Cheat & Telemetri Real-Time

Pengelola ujian di backend/.../modules/exam/service/ExamRunnerService.java memberlakukan validasi ketat di sisi server sehingga peserta tidak dapat memanipulasi waktu ujian melalui jam laptop/HP:

Perhitungan Sisa Waktu yang Tersinkronisasi

JAVA

private long calculateRemainingSeconds(Examination exam, ExamAttempt attempt, LocalDateTime now) {
    LocalDateTime examDeadline = attempt.getStartedAt().plusMinutes(exam.getDurationMinutes());
    
    // Batas mutlak: durasi peserta tidak boleh melewati jadwal penutupan ujian serentak
    if (exam.getEndAt() != null && exam.getEndAt().isBefore(examDeadline)) {
        examDeadline = exam.getEndAt();
    }
    
    return Math.max(0, Duration.between(now, examDeadline).getSeconds());
}

Pengawasan Anti-Cheat dengan WebSocket STOMP

Pada frontend React di frontend/src/pages/exams/ExamRunner.tsx:

  • Menu klik kanan dinonaktifkan (e.preventDefault()).
  • Deteksi perpindahan tab atau minimalkan aplikasi memanfaatkan HTML5 visibilitychange API.
  • Pelanggaran keamanan dicatat secara instan ke server. Jika mencapai 5 kali, ujian otomatis di-submit.

TYPESCRIPT

// React 19: Deteksi perpindahan tab & minimalkan jendela ujian
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('Batas pelanggaran keamanan tercapai (5x). Ujian otomatis dikumpulkan.');
          handleSubmitExam();
        }
      });
    }
  };

  document.addEventListener('visibilitychange', handleVisibilityChange);
  return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
}, [submittedResult]);

Pada saat yang bersamaan, backend Spring Boot menyiarkan data telemetri ke layar pengawas guru melalui STOMP:

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()
));

Dashboard pengawas guru (ExamMonitor.tsx) menerima pesan STOMP ini dengan latensi sub-milidetik, memperbarui bar kemajuan siswa dan status pelanggaran secara langsung tanpa membebani server dengan polling berulang.


3. Apache POI: Batch Processing Excel yang Hemat Memori

Mengimpor file spreadsheet berukuran besar (misalnya 5.000 data siswa atau bank 500 soal ujian pilihan ganda) sering kali menyebabkan proses PHP kehabisan memori (memory_limit exceeded).

Pada Spring Boot, kita menggunakan Apache POI dengan pemrosesan berbasis baris:

JAVA

@Transactional
public BatchImportResult importStudentsFromExcel(MultipartFile file, Long classroomId) throws IOException {
    Classroom classroom = classroomRepository.findById(classroomId)
            .orElseThrow(() -> new ResourceNotFoundException("Kelas tidak ditemukan"));
            
    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("Baris " + (i + 1) + ": " + e.getMessage());
            }
        }
    }
    
    return new BatchImportResult(successCount, errors);
}

4. Skema Database: PostgreSQL & Migrasi SQL Flyway

Berbeda dengan Laravel migrations yang ditulis menggunakan kode PHP, arsitektur enterprise Java memanfaatkan file SQL deklaratif yang dikelola secara presisi oleh Flyway.

Dikutip dari 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)
);

Batasan unik (unique constraint) uk_exam_student_attempt mencegah race condition di mana peserta mencoba membuka sesi pengerjaan ganda atau men-submit jawaban dua kali secara bersamaan.


Kapan Memilih Monolit Laravel vs. Java Spring Boot + React?

Pemilihan stack teknologi yang tepat selalu bergantung pada kebutuhan dan skala operasional:

Kebutuhan / Skenario ProyekStack yang DirekomendasikanAlasan & Pertimbangan
Satu Sekolah / Lembaga Kursus (< 1.000 pengguna)Laravel 13 + Livewire + FilamentWaktu time-to-market paling cepat, minim beban operasional DevOps, panel admin siap pakai tanpa koding manual.
Ujian Serentak Distrik / CBT Kampus (> 10.000 pengguna bersamaan)Java 21 Spring Boot + React 19Virtual Threads JVM menangani ribuan I/O konkuren dengan konsumsi memori rendah. API dan antarmuka dapat di-scale secara terpisah.
Kebutuhan Aplikasi Mobile Native (Android / iOS)Java Spring Boot API + ReactSatu backend API REST & WebSocket dapat melayani portal web, aplikasi mobile, maupun lockdown browser.
Tim Developer Tunggal (Solo Developer)Laravel 13 MonolithProduktivitas maksimal tanpa perlu memelihara skema TypeScript ganda, konfigurasi CORS, dan dua pipeline CI/CD terpisah.
Kepatuhan Regulasi & Audit Skala KorporatJava Spring BootPengetikan statis ketat (type safety), otorisasi @PreAuthorize deklaratif, serta dukungan long-term enterprise (LTS).

Repositori Open-Source

Kedua implementasi ini tersedia secara publik untuk dipelajari, dikembangkan, maupun digunakan untuk kebutuhan sekolah dan institusi Anda:


Kesimpulan & Langkah Selanjutnya

Membangun ulang platform Laravel E-Learning ke Java Spring Boot dan React membuktikan bahwa rekayasa perangkat lunak bukanlah soal fanatisme teknologi, melainkan tentang memilih alat yang paling tepat untuk skala permasalahan yang dihadapi. Laravel memberikan kecepatan implementasi yang tiada tanding, sementara Spring Boot dan React menghadirkan ketangguhan konkurensi, kehandalan telemetri proctoring real-time, dan skalabilitas cloud-native.

Pada Part 2, kita akan membedah proses migrasi detail dari skema Eloquent Laravel ke Entity JPA dan pengelolaan migrasi database terkelola dengan Flyway. Jangan lewatkan!

Lanjut Membaca

Artikel Selanjutnya →

Fleet Management System Part 1: Introduction & System Architecture

Next article thumbnail