Latar Belakang & Motivasi: Kecepatan Developer Berpadu Efisiensi Multithreading
Pada artikel sebelumnya di blog ini, saya telah mendokumentasikan perjalanan transformasi aplikasi ujian sekolah tahun 2017 berbasis Laravel 5.2 menuju Laravel 13 modern dengan Filament 3 dan Livewire 3 (tersedia open-source di mfarim/laravel-elearning), serta edisi pendamping enterprise berbasis Java 21 Spring Boot & React dan Python 3.12 FastAPI & React.
Versi monolitik Laravel tersebut terbukti sangat produktif untuk operasional satu sekolah:
- Filament 3 memungkinkan pembuatan panel administrasi CRUD dalam sekejap.
- Livewire 3 menghadirkan pengalaman ujian reaktif tanpa perlu pusing mengelola pipeline build frontend terpisah.
- Pest PHP menjamin seluruh alur pengujian fitur dan penguatan celah IDOR (Insecure Direct Object Reference) berjalan otomatis.
Namun, ketika platform Computer Based Test (CBT) dan Learning Management System (LMS) ini dipersiapkan untuk skala yang lebih luas—seperti jaringan sekolah tingkat kabupaten/distrik, ujian bersama antarlembaga pendidikan, dan lonjakan konkurensi serentak, beberapa pertimbangan arsitektur mulai muncul ke permukaan:
- Lonjakan Konkurensi Saat "Exam Rush Hour": Ketika 2.000 hingga 10.000 peserta serempak mengklik tombol "Mulai Ujian" tepat pukul 08:00:00 WIB, setiap proses worker PHP-FPM mengalokasikan satu thread proses sistem operasi penuh (mengonsumsi RAM 40MB–80MB per worker). Menjalankan ribuan worker sinkron sekaligus memicu kejenuhan memori dan overhead pergantian konteks (context-switching) CPU yang sangat berat.
- Keunggulan Konkurensi Multithreading Puma: Ruby on Rails 8 yang berjalan di atas Puma web server menggabungkan proses worker klaster dengan thread eksekusi konkuren (secara default 5 thread per worker). Model konkurensi hybrid ini memungkinkan Rails menangani banyak kueri database dan panggilan I/O secara bersamaan di dalam satu proses worker tunggal, menghemat konsumsi memori secara signifikan dibanding PHP-FPM.
- Filosofi Legendaris Convention over Configuration:
Ruby on Rails adalah pelopor idiom arsitektur yang menginspirasi berbagai framework web modern—termasuk Laravel sendiri. Active Record adalah salah satu implementasi Object-Relational Mapping (ORM) paling ekspresif dan elegan dalam rekayasa perangkat lunak. Dengan mengaktifkan Rails 8 dalam mode API-only (
--api), kita memangkas middleware aset, sesi, dan cookie yang tidak diperlukan untuk menghasilkan backend JSON yang sangat ramping dengan kecepatan pengembangan (developer velocity) tiada tanding. - Telemetri Pengawasan Real-Time via Action Cable: Guru pengawas di ruang kontrol memerlukan telemetri langsung (sisa waktu ujian, status palet nomor soal yang telah dijawab, serta peringatan pelanggaran kecurangan seperti keluar layar penuh atau ganti tab). Rails menyediakan integrasi WebSocket kelas satu melalui Action Cable dan Redis Pub/Sub, menghadirkan komunikasi dua arah tanpa beban polling dari sisi klien.
- Ekosistem Multi-Platform yang Terpisah: Lembaga pendidikan modern memerlukan antarmuka web modern dan aplikasi mobile khusus (Android/iOS) yang mengakses endpoint REST API dan WebSocket yang sama dengan tipe data yang konsisten.
Untuk menjawab kebutuhan operasional tersebut, saya membangun edisi pendamping keempat dalam series ini: Ruby on Rails 8 + React E-Learning.
Perbandingan Arsitektur: Monolit vs. Layanan Rails Terpisah
Berikut adalah perbandingan mendalam bagaimana setiap lapisan arsitektur bertransformasi dari monolit Laravel ke edisi terpisah Ruby on Rails 8 + React:
| Aspek Arsitektur | Edisi Monolitik Laravel | Edisi Ruby on Rails 8 + React |
|---|---|---|
| Bahasa & Runtime | PHP 8.3 (Interpreted / OPcache), model proses-per-request PHP-FPM | Ruby 3.3+ dengan YJIT, Puma multithreaded clustered server (5 thread/worker) |
| Framework Backend | Laravel 13 Monolith | Ruby on Rails 8.1 (Mode API-only, middleware minimalis) |
| Framework Frontend | Template Blade + Komponen reaktif Livewire 3 | React 19 SPA + TypeScript + Vite 6 + Tailwind CSS v4 |
| Serialisasi Data | Laravel API Resource (JsonResource) | Serialisasi Active Record dengan konversi otomatis snake_case ke camelCase |
| Otentikasi | Stateful PHP session cookies + token CSRF | Stateless JWT Bearer token dengan Engine Impersonasi Administratif 1-Klik |
| Otorisasi (RBAC) | Laravel Gate & Model Policy (#[Authorize]) | Guard Controller (require_admin!, require_teacher!, require_student!) |
| Database & Migrasi | MySQL 8 dengan migrasi Artisan Laravel | PostgreSQL 16 dengan migrasi Active Record (db:migrate) |
| Lapisan Caching | Cache Redis via fasad Cache Laravel | Redis 7 via connection pool gem redis |
| Telemetri Real-Time | Laravel Reverb / Klien Echo | Action Cable WebSocket (/cable) dengan adapter Redis Pub/Sub |
| Pemrosesan Batch | Antrean Laravel + PhpSpreadsheet | Gem roo streaming batch parser untuk template .xlsx siswa & bank soal |
| Generasi Dokumen | PhpSpreadsheet / DomPDF | Gem caxlsx untuk kartu ujian peserta ber-barcode |
| Pengujian Otomatis | Pest PHP + Laravel TestCase | RSpec (rspec-rails 7.1) + FactoryBot + DatabaseCleaner |
| Standar Kualitas Kode | Laravel Pint (PHP-CS-Fixer) | RuboCop Rails Omakase (rubocop-rails-omakase) |
| Model Deployment | Container tunggal / unit systemd Linux VPS | Multi-container Docker Compose (Nginx, React, Rails API, Postgres, Redis) |
Blueprint Arsitektur Sistem
Berikut adalah arsitektur cloud-ready yang diimplementasikan pada repositori Ruby on Rails 8 + React:
Sorotan Teknis & Pembahasan Mendalam (Deep Dives)
Deep Dive 1: Otentikasi Stateless JWT & Engine Impersonasi 1-Klik
Pada operasional sekolah, administrator kerap kali perlu melakukan troubleshooting langsung saat siswa mengalami kendala di tengah sesi ujian atau memverifikasi tampilan materi dari sudut pandang guru. Alih-alih meminta kata sandi pengguna atau mengubah database secara manual, backend Rails menyediakan engine impersonasi administratif 1-klik:
RUBY
# backend/app/services/json_web_token.rb
class JsonWebToken
SECRET_KEY = ENV.fetch("SECRET_KEY") {
Rails.application.secret_key_base || "ruby-rails-elearning-secret-key-2026-super-secure"
}
def self.encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, SECRET_KEY, 'HS256')
end
def self.decode(token)
decoded = JWT.decode(token, SECRET_KEY, true, { algorithm: 'HS256' })[0]
HashWithIndifferentAccess.new(decoded)
rescue JWT::DecodeError
nil
end
end
Saat admin mengklik tombol Impersonate di dashboard React, permintaan dikirim ke AuthController#impersonate. API Rails memastikan izin administratif, lalu menerbitkan JWT baru atas nama target pengguna dengan mencatat ID admin asli pada klaim impersonated_by:
RUBY
# backend/app/controllers/api/v1/auth_controller.rb
module Api
module V1
class AuthController < BaseController
skip_before_action :authenticate_user!, only: [:login, :register]
def impersonate
require_admin!
return if performed?
target_user = User.find(params[:user_id])
token = JsonWebToken.encode(user_id: target_user.id)
payload = build_auth_payload(target_user, token, impersonated_by: current_user.id)
json_response(data: payload, message: "Impersonation started successfully")
end
def stop_impersonate
admin_user = User.find_by(role: 'ROLE_ADMIN') || current_user
token = JsonWebToken.encode(user_id: admin_user.id)
payload = build_auth_payload(admin_user, token)
json_response(data: payload, message: "Returned to admin session")
end
private
def build_auth_payload(user, token, impersonated_by: nil)
{
accessToken: token,
refreshToken: token,
tokenType: "Bearer",
userId: user.id,
name: user.name,
email: user.email,
roles: [user.role],
active: user.active,
impersonatedBy: impersonated_by || user.impersonated_by_id,
token: token
}
end
end
end
end
Untuk menjembatani perbedaan konvensi penamaan antara model Ruby yang menggunakan snake_case dengan TypeScript frontend yang menggunakan camelCase, BaseController melakukan transformasi otomatis pada seluruh respon JSON:
RUBY
# backend/app/controllers/api/v1/base_controller.rb
def json_response(data: nil, message: "Success", status: :ok, errors: nil)
camelized_data = camelize_keys(data.as_json)
render json: {
success: [:ok, :created, 200, 201].include?(status),
message: message,
data: camelized_data,
errors: errors,
timestamp: Time.current.iso8601
}, status: status
end
def camelize_keys(obj)
case obj
when Hash
obj.each_with_object({}) do |(k, v), result|
result[k.to_s.camelize(:lower)] = camelize_keys(v)
end
when Array
obj.map { |item| camelize_keys(item) }
else
obj
end
end
Deep Dive 2: Engine Ujian CBT Konkurensi Tinggi & Pelacakan Anti-Cheat
Engine Computer Based Testing di ExamRunnerController mengelola sesi ujian serentak dengan penegakan timer otoritatif dari sisi server dan pencatatan pelanggaran integritas:
- Timer Otoritatif di Server: Sisa waktu ujian tidak pernah bergantung pada jam perangkat siswa (menghindari kecurangan manipulasi jam lokal). Saat siswa memulai atau me-refresh ujian, sisa waktu dihitung murni di server:
RUBY
elapsed = (Time.current - attempt.started_at).to_i remaining = [exam.duration_minutes * 60 - elapsed, 0].max - Auto-Save Jawaban Seketika: Setiap kali siswa memilih opsi pada tampilan soal React, permintaan asinkron langsung menyimpan jawaban via
find_or_initialize_by:RUBY
# backend/app/controllers/api/v1/exam_runner_controller.rb def answer require_student! return if performed? attempt = ExamAttempt.find(params[:attempt_id]) return render_error("Attempt is already submitted") if attempt.status != 'in_progress' question = Question.find(params[:questionId]) ans = ExamAnswer.find_or_initialize_by(exam_attempt: attempt, question: question) ans.student_answer = params[:answer].to_s ans.save! json_response(data: nil, message: "Answer saved") end - Pencatatan Pelanggaran Anti-Cheat: Ketika antarmuka React mendeteksi siswa beralih tab, meminimalkan browser, atau keluar dari mode layar penuh (fullscreen), pelanggaran langsung dicatat ke server:
RUBY
def violation require_student! return if performed? attempt = ExamAttempt.find(params[:attempt_id]) attempt.increment!(:violations_count) json_response(data: { violations: attempt.violations_count }, message: "Violation recorded") end - Pipeline Submit & Penilaian Otomatis: Saat waktu habis atau siswa mengklik tombol Selesai, soal pilihan ganda dan benar/salah dinilai secara instan. Jika terdapat soal esai, status ujian ditandai sebagai
needs_gradingagar guru dapat memberikan nilai esai secara manual:RUBY
def submit require_student! return if performed? attempt = ExamAttempt.find(params[:attempt_id]) exam = attempt.examination questions = exam.questions.index_by(&:id) answers = attempt.exam_answers.includes(:question) total_points = questions.values.sum(&:points) earned_points = 0.0 has_essay = false answers.each do |ans| q = ans.question next unless q if q.question_type == 'multiple_choice' || q.question_type == 'true_false' is_correct = ans.student_answer.to_s.strip.downcase == q.correct_answer.to_s.strip.downcase ans.update!( is_correct: is_correct, points_awarded: is_correct ? q.points : 0.0, graded_at: Time.current ) earned_points += q.points if is_correct elsif q.question_type == 'essay' has_essay = true end end score_pct = total_points > 0 ? (earned_points / total_points * 100.0).round(2) : 0.0 passed = score_pct >= exam.passing_score attempt.update!( score: score_pct, passed: passed, finished_at: Time.current, status: has_essay ? 'needs_grading' : 'completed', remaining_seconds: 0 ) json_response(data: { score: attempt.score, passed: attempt.passed, status: attempt.status, totalQuestions: questions.size, answeredCount: answers.size }, message: "Exam submitted successfully") end
Deep Dive 3: Pemrosesan Batch Excel Streaming dengan Gem Roo
Mendaftarkan ratusan siswa baru dan mengimpor puluhan bank soal tidak boleh memblokir thread worker Puma. Dengan menggunakan gem roo, backend membaca file .xlsx, .xls, dan .csv secara efisien tanpa pemborosan memori:
RUBY
# backend/app/services/excel_service.rb
require 'roo'
class ExcelService
def self.parse_students(file_path)
spreadsheet = open_spreadsheet(file_path)
header = spreadsheet.row(1).map { |h| h.to_s.strip.downcase.underscore }
rows = []
(2..spreadsheet.last_row).each do |i|
row = Hash[[header, spreadsheet.row(i)].transpose]
next if row['name'].blank? || row['nis'].blank?
rows << row
end
rows
end
def self.parse_questions(file_path)
spreadsheet = open_spreadsheet(file_path)
header = spreadsheet.row(1).map { |h| h.to_s.strip.downcase.underscore }
rows = []
(2..spreadsheet.last_row).each do |i|
row = Hash[[header, spreadsheet.row(i)].transpose]
next if row['question_text'].blank?
rows << row
end
rows
end
private
def self.open_spreadsheet(file_path)
ext = File.extname(file_path).downcase
case ext
when '.xlsx' then Roo::Excelx.new(file_path)
when '.xls' then Roo::Excel.new(file_path)
when '.csv' then Roo::CSV.new(file_path)
else
raise "Unsupported file format: #{ext}. Please upload .xlsx, .xls, or .csv"
end
end
end
Pada StudentsController, proses impor dijalankan di dalam blok transaksi PostgreSQL (User.transaction do ... end). Akun User dan profil relasional Student dibuat secara atomik sehingga mencegah terjadinya data yatim (orphaned records) apabila terdapat anomali pada baris data:
RUBY
# backend/app/controllers/api/v1/students_controller.rb
def import_excel
require_admin!
return if performed?
file = params[:file]
return render_error("No file uploaded") unless file
rows = ExcelService.parse_students(file.tempfile.path)
imported_count = 0
User.transaction do
rows.each do |r|
user = User.find_or_create_by!(email: "#{r['nis']}@elearning.test") do |u|
u.name = r['name']
u.password = "password123"
u.role = 'ROLE_STUDENT'
u.active = true
end
student = Student.find_or_initialize_by(user: user)
student.name = r['name']
student.email = user.email
student.nis = r['nis']
student.gender = r['gender'].presence || 'M'
student.classroom_id = params[:classroom_id] if params[:classroom_id].present?
student.active = true
student.save!
imported_count += 1
end
end
json_response(data: { importedCount: imported_count }, message: "Imported #{imported_count} students successfully")
end
Deep Dive 4: Frontend Modern Terpisah (React 19 + TypeScript + Vite 6)
Aplikasi klien frontend dibangun sebagai Single Page Application (SPA) modern menggunakan:
- React 19: Memanfaatkan fitur hooks fungsional terbaru.
- TypeScript 5: Menjamin type safety ketat di seluruh kontrak API dan DTO.
- Tailwind CSS v4: Didukung oleh engine native
@tailwindcss/viteuntuk build super cepat. - Zustand 5: State management terpusat untuk sesi otentikasi dan runner ujian (
useAuthStore). - Axios Interceptor: Penempelan token JWT otomatis dan pemulihan sesi saat impersonasi.
TYPESCRIPT
// frontend/src/services/api.ts
import axios from 'axios';
import { useAuthStore } from '../store/authStore';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1',
headers: {
'Content-Type': 'application/json',
},
});
api.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
useAuthStore.getState().logout();
}
return Promise.reject(error);
}
);
export default api;
Tampilan Runner Ujian Siswa (Mobile-First)
Komponen runner ujian siswa didesain responsif dengan fokus mobile-first:
- Palet Soal Dinamis: Tombol nomor soal berwarna hijau (telah dijawab) dan abu-abu (belum dijawab).
- Countdown Tersinkronisasi: Menjalankan auto-submit otomatis saat sisa waktu mencapai 00:00:00.
- Listener Anti-Cheat: Menangkap event browser seperti
document.addEventListener('visibilitychange')danwindow.addEventListener('blur')untuk mengirim notifikasi pelanggaran ke server Rails.
Matriks Perbandingan 4 Framework Arsitektur
Dengan hadirnya empat edisi pada series ini, berikut adalah perbandingan komprehensif seluruh framework yang telah dibangun:
| Parameter Arsitektur | Laravel 13 Monolith | Java 21 Spring Boot 3.4 | Python 3.12 FastAPI | Ruby on Rails 8.1 API |
|---|---|---|---|---|
| Pola Arsitektur | Server-Side Monolith | Decoupled REST + SPA | Decoupled Async REST + SPA | Decoupled REST + SPA |
| Model Konkurensi | Process-per-request (PHP-FPM) | Virtual Threads (Project Loom) | Non-blocking Event Loop (ASGI) | Multithreaded Klaster (Puma) |
| Konsumsi RAM Worker | 40MB – 80MB | 200MB – 400MB (JVM Base) | 15MB – 35MB | 50MB – 90MB (5 thread) |
| Lapisan Akses Data | Eloquent ORM (Active Record) | Spring Data JPA + Hibernate | SQLAlchemy 2.0 Asyncpg | Active Record (Ruby) |
| Verifikasi Tipe Data | Dinamis / PHPStan Level 8 | Statis (Kompilasi Java) | Runtime Pydantic v2 (Engine Rust) | Dinamis + Kontrak RSpec |
| Protokol Real-Time | Reverb / Pusher WebSockets | STOMP via SockJS (/ws/cbt) | Native ASGI WebSockets | Action Cable + Redis PubSub |
| Kecepatan Developer | ★★★★★ (Filament CRUD Instan) | ★★★☆☆ (Boilerplate Ketat) | ★★★★☆ (Routing Cepat Typed) | ★★★★★ (Convention over Config) |
| Kesesuaian Kasus | Satu sekolah, MVP cepat | Sistem enterprise kepatuhan tinggi | LMS berbasis AI & data science | Startup berkembang cepat |
Panduan Menjalankan dengan Docker Compose
Seluruh arsitektur (PostgreSQL 16, Redis 7, Rails 8 API, dan Frontend React 19) dapat dijalankan dalam beberapa langkah mudah:
BASH
# 1. Clone repositori
git clone https://github.com/mfarim/ruby-rails-elearning-react.git
cd ruby-rails-elearning-react
# 2. Salin konfigurasi environment
cp .env.example .env
# 3. Jalankan seluruh container
docker compose up -d --build
# 4. Pantau log backend Rails
docker compose logs -f backend
Setelah container aktif, buka di browser:
- Aplikasi Web (React 19): http://localhost:3002
- Endpoint Health API: http://localhost:8000/health
Kredensial Akun Uji Coba (Demo)
| Peran (Role) | Password | |
|---|---|---|
| Administrator | [email protected] | password123 |
| Guru (Teacher) | [email protected] | password123 |
| Siswa (Student) | [email protected] | password123 |
Menjalankan Test Suite (RSpec)
Backend Rails dilengkapi rangkaian pengujian otomatis untuk memverifikasi otentikasi, alur ujian, dan transaksi Active Record:
BASH
cd backend
bundle install
bundle exec rspec
Kesimpulan & Langkah Selanjutnya
Membangun ulang platform Laravel E-Learning ke dalam Ruby on Rails 8 dan React membuktikan bahwa kecepatan pengembangan (developer happiness) tidak harus mengorbankan skalabilitas arsitektur:
- Mode API Rails 8 menyediakan backend JSON yang bersih dan bebas dari beban pipeline monolitik lama.
- Arsitektur multithreaded Puma menyerap lonjakan konkurensi ujian serentak dengan efisiensi memori tinggi.
- Otentikasi stateless JWT dengan impersonasi administratif 1-klik mempermudah pengawasan sesi ujian siswa.
- Active Record dan streaming batch Roo menjaga logika domain tetap bersih, ekspresif, dan mudah dirawat.
Pada seri berikutnya, kita akan membahas detail migrasi skema database, relasi kompleks Active Record, konfigurasi channel real-time Action Cable, hingga optimasi performa container di lingkungan produksi.
Kunjungi repositori open-source pendamping:
- Edisi Ruby on Rails + React: github.com/mfarim/ruby-rails-elearning-react
- Edisi Python FastAPI + React: github.com/mfarim/fastapi-elearning-react
- Edisi Java Spring Boot + React: github.com/mfarim/spring-elearning-react
- Edisi Monolitik Laravel 13: github.com/mfarim/laravel-elearning

