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:
- 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.
- 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.
- 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).
- 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.
Perbandingan Arsitektur: Monolit vs. Layanan Terpisah (Decoupled)
Berikut adalah perbandingan mendalam antara kedua pendekatan arsitektur:
| Aspek Arsitektur | Edisi Monolitik Laravel | Edisi Java Spring Boot + React |
|---|---|---|
| Bahasa & Runtime | PHP 8.3 (Interpreted / JIT), model eksekusi proses-per-request via PHP-FPM | Java 21 LTS (HotSpot JVM + Virtual Threads / Project Loom) |
| Framework Backend | Laravel 13 Monolith | Spring Boot 3.4.3 (RESTful API micro-architecture) |
| Framework Frontend | Blade templates + komponen reaktif Livewire 3 | React 19 SPA + TypeScript + Vite 6 + Tailwind CSS v4 |
| Autentikasi | Cookie session stateful + token CSRF | Token Stateless JWT Bearer dengan Fitur Impersonasi |
| Otorisasi & Kebijakan | Laravel Gate & Model Policies (#[Authorize]) | Spring Security 6 Filter Chain + method security @PreAuthorize |
| Database & Migrasi | MySQL 8 dengan Laravel Artisan Migrations | PostgreSQL 16 dengan versioned SQL migrations via Flyway (V1__...) |
| Layer Caching | Cache Redis via Laravel Cache facade | Cache Redis 7 via Spring Data Redis & Lettuce connection pool |
| Telemetri Real-Time | Laravel Reverb / Echo client | Spring WebSocket STOMP message broker (/ws/cbt) + SockJS |
| Batch Processing | Laravel Queues + PhpSpreadsheet | Apache POI streaming batch reader untuk berkas .xlsx siswa & soal |
| Dokumentasi API | Scramble / Scribe OpenAPI generator | Springdoc OpenAPI 3 + Swagger UI interactive explorer |
| Model Deployment | Container tunggal / VPS Linux systemd unit | Multi-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:
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
visibilitychangeAPI. - 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 Proyek | Stack yang Direkomendasikan | Alasan & Pertimbangan |
|---|---|---|
| Satu Sekolah / Lembaga Kursus (< 1.000 pengguna) | Laravel 13 + Livewire + Filament | Waktu 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 19 | Virtual 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 + React | Satu backend API REST & WebSocket dapat melayani portal web, aplikasi mobile, maupun lockdown browser. |
| Tim Developer Tunggal (Solo Developer) | Laravel 13 Monolith | Produktivitas maksimal tanpa perlu memelihara skema TypeScript ganda, konfigurasi CORS, dan dua pipeline CI/CD terpisah. |
| Kepatuhan Regulasi & Audit Skala Korporat | Java Spring Boot | Pengetikan 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:
- 🐘 Edisi Monolitik Laravel: https://github.com/mfarim/laravel-elearning
- Laravel 13, Livewire 3, Filament 3, MySQL, Pest PHP
- ☕ Edisi Java Spring Boot + React: 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
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!
