The Motivation: Developer Velocity Meets Multithreaded Efficiency
Earlier in this blog series, I documented the complete evolution 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), followed by enterprise companion editions built with Java 21 Spring Boot & React and Python 3.12 FastAPI & React.
The monolithic Laravel platform remains exceptionally productive for standard school operations:
- Filament 3 empowers administrators with instant administrative CRUD dashboards.
- Livewire 3 brings reactive student exam timers and question switching without requiring separate frontend build toolchains.
- Pest PHP delivers automated regression testing and rigorous IDOR security hardening.
However, as Computer Based Testing (CBT) and Learning Management Systems (LMS) scale to district-wide academic networks, inter-school assessment federations, and high-concurrency examination events, specific architectural trade-offs become evident:
- The "Exam Rush Hour" Concurrency Surge: When 2,000 to 10,000 candidates click "Start Exam" at precisely 08:00:00 AM, traditional synchronous PHP-FPM workers allocate one full operating system process per request (consuming 40MB–80MB resident RAM per worker). Spawning thousands of synchronous workers causes memory exhaustion, heavy process thrashing, and high infrastructure bills.
- Puma's Multithreaded Concurrency Advantage: Ruby on Rails 8 running on the Puma web server combines clustered worker processes with concurrent execution threads (defaulting to 5 threads per worker). This hybrid concurrency model enables Rails to handle multiple simultaneous I/O-bound database queries and network calls within a single worker process, dramatically slashing memory consumption compared to PHP-FPM.
- The Heritage of Convention over Configuration:
Ruby on Rails pioneered the architectural idioms that inspired modern web frameworks—including Laravel itself. Active Record remains one of the most expressive, elegant Object-Relational Mappings in computer science. By utilizing Rails 8 in API-only mode (
--api), we discard legacy asset pipelines, sessions, and cookie middleware to create an ultra-lean JSON backend with unmatched developer velocity. - Real-Time Proctoring & Push Telemetry: Teachers supervising live examination halls require continuous telemetry: countdown timers, answered question palettes, and anti-cheat violation flags (fullscreen exit, blur events). Rails provides first-class real-time WebSocket communication through Action Cable and Redis Pub/Sub, enabling bidirectional live updates with zero client-side polling overhead.
- Decoupled Multi-Platform Client Ecosystem: Modern academic institutions require standalone mobile applications (iOS/Android) and modern web clients consuming unified, typed REST and WebSocket endpoints.
To address these operational demands, I engineered the fourth companion edition in this series: Ruby on Rails 8 + React E-Learning.
Architectural Comparison: Monolith vs. Decoupled Rails Service
Here is how each critical architectural layer transitions between the monolithic Laravel foundation and the decoupled Ruby on Rails 8 + React platform:
| Architectural Concern | Laravel Monolithic Edition | Ruby on Rails 8 + React Edition |
|---|---|---|
| Language & Runtime | PHP 8.3 (Interpreted / OPcache), PHP-FPM process-per-request | Ruby 3.3+ with YJIT, Puma multithreaded clustered server (5 threads/worker) |
| Backend Framework | Laravel 13 Monolith | Ruby on Rails 8.1 (API-only mode, stripped middleware) |
| Frontend Framework | Blade templates + Livewire 3 reactive server components | React 19 SPA + TypeScript + Vite 6 + Tailwind CSS v4 |
| Data Serialization | Laravel API Resources (JsonResource) | Active Record serialization with automated snake_case to camelCase mapping |
| Authentication | Stateful PHP session cookies + CSRF tokens | Stateless JWT Bearer tokens with 1-Click Administrative Impersonation Engine |
| Authorization | Laravel Gate & Model Policies (#[Authorize]) | Controller guards (require_admin!, require_teacher!, require_student!) |
| Database & Migrations | MySQL 8 with Laravel Artisan migrations | PostgreSQL 16 with Active Record migrations (db:migrate) |
| Caching Layer | Redis cache via Laravel Cache facade | Redis 7 via redis gem connection pool |
| Real-Time Telemetry | Laravel Reverb / Echo client | Action Cable WebSockets (/cable) with Redis Pub/Sub adapter |
| Batch Processing | Laravel Queues + PhpSpreadsheet | roo gem streaming batch parser for student & question .xlsx templates |
| Spreadsheet Generation | PhpSpreadsheet / DomPDF | caxlsx gem for server-side candidate exam tickets & hall passes |
| Automated Testing | Pest PHP + Laravel TestCase | RSpec (rspec-rails 7.1) + FactoryBot + DatabaseCleaner |
| Code Quality | Laravel Pint (PHP-CS-Fixer) | RuboCop Rails Omakase (rubocop-rails-omakase) |
| Deployment Model | Single container / Linux VPS systemd unit | Multi-container Docker Compose (Nginx, React, Rails API, Postgres, Redis) |
System Architecture Blueprint
Below is the decoupled cloud-ready architecture implemented in the Ruby on Rails 8 + React repository:
Core Technical Highlights & Deep Dives
Deep Dive 1: Stateless JWT Authentication & 1-Click Impersonation
In enterprise educational environments, school administrators frequently need to troubleshoot student exam issues mid-session or inspect curriculum views configured by specific teachers. Rather than requiring shared passwords or temporary database alterations, the Rails backend implements a 1-click administrative impersonation engine:
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
When an administrator clicks the Impersonate button in the React dashboard, the request hits AuthController#impersonate. The Rails API verifies administrative authorization and signs a new JWT containing the target user's identity, tracking the original administrator's ID in the impersonated_by claim:
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
To eliminate casing mismatch between Ruby's idiomatic snake_case models and React's TypeScript camelCase conventions, BaseController automatically transforms the JSON payload:
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: High-Concurrency CBT Exam Runner & Anti-Cheat Engine
The Computer Based Testing engine in ExamRunnerController handles high-concurrency exam attempts with absolute server-side time enforcement and anti-cheat tracking:
- Authoritative Server Timer: The remaining exam duration is never trusted to the client clock. When the student initiates or reloads the exam, elapsed time is computed strictly on the server:
RUBY
elapsed = (Time.current - attempt.started_at).to_i remaining = [exam.duration_minutes * 60 - elapsed, 0].max - Instant Answer Auto-Save: When a student selects an option on the React question runner, an asynchronous request immediately persists the answer using Active Record's
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 - Anti-Cheat Violation Recording: When the React exam runner detects a window blur, tab switch, or fullscreen exit, the violation event is incremented server-side:
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 - Automated Submission & Grading Pipeline: When the timer expires or the candidate clicks Submit, multiple choice and true/false questions are evaluated automatically. If essay questions are present, the attempt status transitions to
needs_gradingfor teacher review: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: Streaming Batch Excel Processing with Roo
Enrolling hundreds of students and importing question banks with dozens of multiple-choice options must not block Puma worker threads. Using the lightweight roo gem, the backend reads .xlsx, .xls, and .csv files as streaming iterators:
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
In StudentsController, the imported data is wrapped in a PostgreSQL transaction (User.transaction do ... end). User accounts and associated Student profiles are provisioned atomically, ensuring zero orphaned records if any row contains invalid 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: Modern Decoupled Frontend (React 19 + TypeScript + Vite 6)
The frontend client is engineered as a modern Single Page Application (SPA) using:
- React 19: Utilizing modern hooks and functional patterns.
- TypeScript 5: Strict typing across all API contracts and DTOs.
- Tailwind CSS v4: Powered by the native
@tailwindcss/viteengine for zero-runtime CSS generation. - Zustand 5: Centralized authentication and exam state management (
useAuthStore). - Axios Interceptors: Transparent JWT injection and automatic session restoration upon impersonation.
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;
Student Examination Runner Screen
The student exam runner component enforces full-screen proctoring and real-time auto-saving:
- Dynamic Question Palette: Color-coded buttons showing answered (green) vs remaining (gray) questions.
- Synchronized Countdown: Automatically fires
submitExam()when the remaining seconds reach zero. - Anti-Cheat Listeners: Captures
document.addEventListener('visibilitychange')andwindow.addEventListener('blur')to increment violation counters on the Rails server.
4-Way Architectural Comparison Matrix
With four editions now completed in this series, here is how each framework stacks up across key architectural vectors:
| Architectural Metric | Laravel 13 Monolith | Java 21 Spring Boot 3.4 | Python 3.12 FastAPI | Ruby on Rails 8.1 API |
|---|---|---|---|---|
| Architecture | Server-Side Monolith | Decoupled REST + SPA | Decoupled Async REST + SPA | Decoupled REST + SPA |
| Concurrency Model | Process-per-request (PHP-FPM) | Virtual Threads (Project Loom) | Non-blocking Event Loop (ASGI) | Clustered Multithreaded (Puma) |
| Memory per Worker | 40MB – 80MB | 200MB – 400MB (JVM Base) | 15MB – 35MB | 50MB – 90MB (5 threads) |
| Data Access Layer | Eloquent ORM (Active Record) | Spring Data JPA + Hibernate | SQLAlchemy 2.0 Asyncpg | Active Record (Ruby) |
| Type Verification | Dynamic / PHPStan Level 8 | Static (Java Compile-time) | Runtime Pydantic v2 (Rust engine) | Dynamic + RSpec contracts |
| Real-Time Protocol | Reverb / Pusher WebSockets | STOMP over SockJS (/ws/cbt) | Native ASGI WebSockets | Action Cable + Redis PubSub |
| Developer Velocity | ★★★★★ (Instant Filament CRUD) | ★★★☆☆ (Strict boilerplate) | ★★★★☆ (Rapid typed routing) | ★★★★★ (Convention over Config) |
| Suitability | Single schools, rapid MVPs | High-security enterprise systems | AI-driven LMS & data science | High-velocity scalable startups |
Quick Start with Docker Compose
You can boot the entire stack (PostgreSQL 16, Redis 7, Rails 8 API, and React 19 Frontend) in under two minutes:
BASH
# 1. Clone the repository
git clone https://github.com/mfarim/ruby-rails-elearning-react.git
cd ruby-rails-elearning-react
# 2. Copy the environment configuration
cp .env.example .env
# 3. Spin up all containers
docker compose up -d --build
# 4. Stream Rails backend logs
docker compose logs -f backend
Once initialized, access the platform:
- Web Application (React 19): http://localhost:3002
- API Health Endpoint: http://localhost:8000/health
Demo Credentials
| Role | Password | |
|---|---|---|
| Administrator | [email protected] | password123 |
| Teacher | [email protected] | password123 |
| Student | [email protected] | password123 |
Running the Test Suite (RSpec)
The backend features an automated test suite verifying authentication, exam runners, and Active Record transactions:
BASH
cd backend
bundle install
bundle exec rspec
Conclusion & Series Next Steps
Rebuilding the Laravel E-Learning platform into Ruby on Rails 8 and React proves that developer velocity does not have to come at the expense of architectural scalability:
- Rails 8 API Mode delivers a clean, lean JSON backend without frontend bloat.
- Puma's multithreaded architecture easily absorbs high-concurrency exam rushes.
- Stateless JWTs with 1-click administrative impersonation streamline exam hall support.
- Active Record and Roo streaming ensure clean, maintainable domain code.
In the upcoming installments of this series, we will dive deeper into the database migrations, Active Record domain modeling, Action Cable real-time channels, and production deployment tuning.
Explore the open-source companion repositories:
- Ruby on Rails + React Edition: github.com/mfarim/ruby-rails-elearning-react
- Python FastAPI + React Edition: github.com/mfarim/fastapi-elearning-react
- Java Spring Boot + React Edition: github.com/mfarim/spring-elearning-react
- Laravel 13 Monolith Edition: github.com/mfarim/laravel-elearning

