Pertanyaan Wawancara Ruby on Rails: Top 25 di 2026
25 pertanyaan wawancara Ruby on Rails yang paling sering ditanyakan. Arsitektur MVC, Active Record, migrasi, pengujian RSpec, REST API dengan jawaban detail dan contoh kode.

Wawancara Ruby on Rails menilai penguasaan framework Ruby paling populer, pemahaman arsitektur MVC, ORM Active Record, dan kemampuan membangun aplikasi web yang kokoh dengan filosofi "Convention over Configuration". Panduan ini mencakup 25 pertanyaan yang paling sering ditanyakan, dari dasar Rails hingga pola produksi tingkat lanjut.
Recruiter menghargai kandidat yang memahami filosofi Rails: "Convention over Configuration", DRY (Don't Repeat Yourself), dan pola Rails Way. Menjelaskan mengapa Rails membuat keputusan arsitektur tertentu dapat membuat perbedaan.
Dasar-dasar Ruby on Rails
Pertanyaan 1: Jelaskan pola MVC di Ruby on Rails
Pola Model-View-Controller (MVC) merupakan inti arsitektur Rails. Pola ini memisahkan tanggung jawab menjadi tiga lapisan berbeda untuk meningkatkan keterbacaan dan kemudahan pengujian kode.
# app/models/article.rb
# Model mengelola data dan logika bisnis
class Article < ApplicationRecord
# Validasi data
validates :title, presence: true, length: { minimum: 5 }
validates :body, presence: true
# Asosiasi dengan model lain
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
has_many :tags, through: :article_tags
# Scope untuk query yang dapat digunakan ulang
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
# Callback siklus hidup
before_save :generate_slug
private
def generate_slug
self.slug = title.parameterize if title_changed?
end
end# app/controllers/articles_controller.rb
# Controller menerima permintaan dan mengatur respons
class ArticlesController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_article, only: [:show, :edit, :update, :destroy]
def index
@articles = Article.published.recent.includes(:author)
end
def show
@comments = @article.comments.includes(:user)
end
def create
@article = current_user.articles.build(article_params)
if @article.save
redirect_to @article, notice: 'Artikel berhasil dibuat.'
else
render :new, status: :unprocessable_entity
end
end
private
def set_article
@article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:title, :body, :published, tag_ids: [])
end
end<%# app/views/articles/show.html.erb %>
<%# View menampilkan data dalam format HTML %>
<article class="article-detail">
<header>
<h1><%= @article.title %></h1>
<p class="meta">
Oleh <%= @article.author.name %> •
<%= l @article.created_at, format: :long %>
</p>
</header>
<div class="content">
<%= simple_format @article.body %>
</div>
<%# Partial untuk komentar %>
<%= render @comments %>
</article>Alur tipikal: permintaan tiba di Router, yang meneruskannya ke Controller yang sesuai. Controller berinteraksi dengan Model untuk mengambil atau memodifikasi data, lalu meneruskan data tersebut ke View untuk render HTML.
Pertanyaan 2: Apa itu Active Record dan bagaimana ORM Rails bekerja?
Active Record adalah ORM (Object-Relational Mapping) Rails yang mengimplementasikan pola Active Record. Setiap kelas Model mewakili sebuah tabel basis data, dan setiap instance mewakili satu baris.
# app/models/user.rb
# Active Record memetakan kolom ke atribut secara otomatis
class User < ApplicationRecord
# Tabel 'users' diasosiasikan secara otomatis
# Kolom: id, email, name, created_at, updated_at
has_secure_password # BCrypt untuk kata sandi
has_many :articles, foreign_key: :author_id
has_one :profile, dependent: :destroy
has_and_belongs_to_many :roles
# Validasi
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
# Callback
before_save :normalize_email
# Metode kelas untuk query
def self.admins
joins(:roles).where(roles: { name: 'admin' })
end
private
def normalize_email
self.email = email.downcase.strip
end
end# Contoh query Active Record
# Konsol Rails atau di dalam service
# Pembuatan
user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123')
# Pembacaan dengan kondisi
active_users = User.where(active: true).order(:name)
user = User.find_by(email: 'dev@example.com')
# Query berantai (lazy evaluation)
recent_admins = User.admins
.where('created_at > ?', 1.month.ago)
.includes(:profile)
.limit(10)
# Pencegahan N+1 dengan eager loading
articles = Article.includes(:author, :comments).published
# Pembaruan
user.update!(name: 'Alice Martin')
# Transaksi
User.transaction do
user.debit_balance!(100)
recipient.credit_balance!(100)
Payment.create!(from: user, to: recipient, amount: 100)
endActive Record mengubah metode Ruby menjadi query SQL yang dioptimalkan. Metode seperti where, joins, dan includes bersifat lazy: query baru dieksekusi saat iterasi atau saat memanggil to_a.
Pertanyaan 3: Jelaskan sistem migrasi Rails
Migrasi memungkinkan versi skema basis data dengan Ruby. Migrasi bersifat reversible dan memungkinkan evolusi struktur data yang terkendali.
# db/migrate/20260203100000_create_products.rb
# Migrasi untuk membuat tabel
class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
t.string :name, null: false
t.text :description
t.decimal :price, precision: 10, scale: 2, null: false
t.integer :stock_quantity, default: 0
t.references :category, null: false, foreign_key: true
t.boolean :active, default: true
t.timestamps # created_at dan updated_at otomatis
end
# Indeks untuk performa
add_index :products, :name
add_index :products, [:category_id, :active]
end
end# db/migrate/20260203110000_add_slug_to_products.rb
# Migrasi untuk memodifikasi tabel yang sudah ada
class AddSlugToProducts < ActiveRecord::Migration[7.1]
def change
add_column :products, :slug, :string
add_index :products, :slug, unique: true
# Mengisi slug yang sudah ada
reversible do |dir|
dir.up do
Product.find_each do |product|
product.update_column(:slug, product.name.parameterize)
end
end
end
# Menjadikan NOT NULL setelah pengisian
change_column_null :products, :slug, false
end
end# Perintah migrasi penting
rails db:migrate # Menjalankan migrasi yang tertunda
rails db:rollback # Membatalkan migrasi terakhir
rails db:rollback STEP=3 # Membatalkan 3 migrasi terakhir
rails db:migrate:status # Melihat status migrasi
rails db:seed # Menjalankan db/seeds.rb
rails db:reset # Drop, create, migrate, seedMigrasi harus reversible. Metode change cerdas dan dapat membalik operasi umum secara otomatis. Untuk kasus kompleks, gunakan up dan down secara terpisah.
Active Record tingkat lanjut
Pertanyaan 4: Bagaimana mengoptimalkan query N+1 di Rails?
Masalah N+1 muncul ketika query awal diikuti oleh N query tambahan untuk memuat asosiasi. Rails menyediakan beberapa metode eager loading untuk mengatasi masalah ini.
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def index
# ❌ MASALAH N+1: 1 query + N query per pesanan
# @orders = Order.all
# Di view: order.user.name menghasilkan satu query per pesanan
# ✅ SOLUSI dengan includes (eager loading)
@orders = Order.includes(:user, :items)
.where(status: 'completed')
.order(created_at: :desc)
# Hanya menghasilkan 3 query secara total
end
def show
# includes: memuat asosiasi terpisah (2-3 query)
@order = Order.includes(items: :product).find(params[:id])
# preload: memaksa pemuatan terpisah
@order = Order.preload(:items, :user).find(params[:id])
# eager_load: memaksa LEFT OUTER JOIN (1 query)
@order = Order.eager_load(:items).find(params[:id])
end
end# app/models/order.rb
class Order < ApplicationRecord
belongs_to :user
has_many :items, class_name: 'OrderItem'
has_many :products, through: :items
# Scope dengan includes default
scope :with_details, -> { includes(:user, items: :product) }
# Counter cache untuk menghindari query COUNT
# Memerlukan: add_column :users, :orders_count, :integer, default: 0
belongs_to :user, counter_cache: true
end# Deteksi N+1 dengan gem Bullet (development)
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.rails_logger = true
end
# Bullet akan menampilkan peringatan ketika:
# - Query N+1 terdeteksi
# - Eager loading tidak diperlukan
# - Counter cache seharusnya digunakanAturannya: gunakan includes secara default (Rails memilih strategi optimal), preload saat ingin memaksa query terpisah, eager_load saat memfilter berdasarkan asosiasi.
Pertanyaan 5: Jelaskan Scope dan Query Object di Rails
Scope mengenkapsulasi kondisi query yang dapat digunakan ulang. Untuk query kompleks, Query Object menawarkan organisasi dan keterujian yang lebih baik.
# app/models/product.rb
class Product < ApplicationRecord
# Scope sederhana
scope :active, -> { where(active: true) }
scope :in_stock, -> { where('stock_quantity > 0') }
scope :featured, -> { where(featured: true) }
# Scope dengan parameter
scope :cheaper_than, ->(price) { where('price < ?', price) }
scope :in_category, ->(category) { where(category: category) }
# Scope berantai
scope :available, -> { active.in_stock }
# Scope dengan joins
scope :with_recent_orders, -> {
joins(:order_items)
.where('order_items.created_at > ?', 30.days.ago)
.distinct
}
# Scope dengan subquery
scope :bestsellers, -> {
where(id: OrderItem.group(:product_id)
.order('COUNT(*) DESC')
.limit(10)
.select(:product_id))
}
end# app/queries/products_search_query.rb
# Query Object untuk pencarian kompleks
class ProductsSearchQuery
def initialize(relation = Product.all)
@relation = relation
end
def call(params)
@relation = filter_by_category(params[:category])
@relation = filter_by_price_range(params[:min_price], params[:max_price])
@relation = filter_by_search(params[:q])
@relation = apply_sorting(params[:sort])
@relation
end
private
def filter_by_category(category)
return @relation if category.blank?
@relation.where(category_id: category)
end
def filter_by_price_range(min, max)
@relation = @relation.where('price >= ?', min) if min.present?
@relation = @relation.where('price <= ?', max) if max.present?
@relation
end
def filter_by_search(query)
return @relation if query.blank?
@relation.where('name ILIKE ? OR description ILIKE ?',
"%#{query}%", "%#{query}%")
end
def apply_sorting(sort)
case sort
when 'price_asc' then @relation.order(price: :asc)
when 'price_desc' then @relation.order(price: :desc)
when 'newest' then @relation.order(created_at: :desc)
else @relation.order(:name)
end
end
end
# Penggunaan di controller
@products = ProductsSearchQuery.new(Product.active).call(params)Scope sangat cocok untuk kondisi sederhana dan dapat digunakan ulang. Query Object cocok untuk pencarian kompleks dengan banyak filter opsional dan logika komposisi.
Siap menguasai wawancara Ruby on Rails Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Routing dan Controller
Pertanyaan 6: Bagaimana cara kerja routing RESTful di Rails?
Rails mendorong rute RESTful yang memetakan kata kerja HTTP ke aksi CRUD. Router menerjemahkan URL menjadi panggilan controller spesifik.
# config/routes.rb
Rails.application.routes.draw do
# Rute RESTful standar (7 aksi)
resources :articles do
# Rute bersarang
resources :comments, only: [:create, :destroy]
# Rute member (bertindak pada satu instance)
member do
post :publish
delete :archive
end
# Rute collection (bertindak pada koleksi)
collection do
get :drafts
get :search
end
end
# Rute API dengan namespace
namespace :api do
namespace :v1 do
resources :products, only: [:index, :show, :create, :update] do
resources :reviews, shallow: true
end
end
end
# Rute kustom
get 'dashboard', to: 'dashboard#index'
# Pembatasan rute
constraints(SubdomainConstraint.new) do
resources :admin_settings
end
# Rute root
root 'home#index'
end# rails routes - Menampilkan semua rute yang dihasilkan
#
# Verb URI Pattern Controller#Action
# GET /articles articles#index
# POST /articles articles#create
# GET /articles/new articles#new
# GET /articles/:id/edit articles#edit
# GET /articles/:id articles#show
# PATCH /articles/:id articles#update
# DELETE /articles/:id articles#destroy
# POST /articles/:id/publish articles#publish
# GET /articles/drafts articles#draftsHelper rute yang dihasilkan (article_path(@article), new_article_path) memungkinkan referensi URL secara dinamis dan mudah dipelihara.
Pertanyaan 7: Jelaskan callback dan filter di controller
Callback (before_action, after_action, around_action) memungkinkan eksekusi kode sebelum, sesudah, atau di sekitar aksi controller.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Perlindungan CSRF aktif secara default
protect_from_forgery with: :exception
# Callback global untuk autentikasi
before_action :authenticate_user!
# Penanganan error global
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def not_found
render json: { error: 'Sumber daya tidak ditemukan' }, status: :not_found
end
def bad_request(exception)
render json: { error: exception.message }, status: :bad_request
end
end# app/controllers/admin/products_controller.rb
class Admin::ProductsController < ApplicationController
# Callback dengan opsi
before_action :require_admin
before_action :set_product, only: [:show, :edit, :update, :destroy]
after_action :log_activity, only: [:create, :update, :destroy]
# Callback bersyarat
before_action :check_stock, only: [:update], if: :stock_changed?
def create
@product = Product.new(product_params)
if @product.save
redirect_to [:admin, @product], notice: 'Produk dibuat.'
else
render :new, status: :unprocessable_entity
end
end
def update
if @product.update(product_params)
redirect_to [:admin, @product], notice: 'Produk diperbarui.'
else
render :edit, status: :unprocessable_entity
end
end
private
def require_admin
redirect_to root_path unless current_user&.admin?
end
def set_product
@product = Product.find(params[:id])
end
def stock_changed?
params[:product][:stock_quantity].present?
end
def log_activity
ActivityLog.create!(
user: current_user,
action: action_name,
resource: @product
)
end
def product_params
params.require(:product).permit(:name, :price, :description, :stock_quantity)
end
endCallback dieksekusi sesuai urutan deklarasi. Gunakan skip_before_action di subclass untuk menonaktifkan callback yang diwariskan. Hindari callback dengan terlalu banyak logika bisnis: lebih baik gunakan Service Object.
Service dan arsitektur
Pertanyaan 8: Bagaimana mengimplementasikan Service Object di Rails?
Service Object mengenkapsulasi logika bisnis kompleks yang bukan milik Model maupun Controller. Service Object meningkatkan keterujian dan mengikuti prinsip single responsibility.
# app/services/order_processor.rb
# Service Object dengan antarmuka standar
class OrderProcessor
def initialize(order, payment_method:)
@order = order
@payment_method = payment_method
end
def call
return failure('Pesanan sudah diproses') if @order.processed?
ActiveRecord::Base.transaction do
validate_stock!
process_payment!
update_inventory!
send_confirmation!
@order.update!(status: 'completed', processed_at: Time.current)
end
success(@order)
rescue PaymentError => e
failure("Pembayaran gagal: #{e.message}")
rescue InsufficientStockError => e
failure("Stok tidak cukup: #{e.message}")
end
private
def validate_stock!
@order.items.each do |item|
unless item.product.stock_quantity >= item.quantity
raise InsufficientStockError, item.product.name
end
end
end
def process_payment!
result = PaymentGateway.charge(
amount: @order.total,
method: @payment_method,
description: "Pesanan ##{@order.id}"
)
raise PaymentError, result.error unless result.success?
@order.update!(payment_reference: result.transaction_id)
end
def update_inventory!
@order.items.each do |item|
item.product.decrement!(:stock_quantity, item.quantity)
end
end
def send_confirmation!
OrderMailer.confirmation(@order).deliver_later
end
def success(data)
Result.new(success: true, data: data)
end
def failure(error)
Result.new(success: false, error: error)
end
Result = Struct.new(:success, :data, :error, keyword_init: true) do
def success? = success
def failure? = !success
end
end# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
@order = current_user.orders.build(order_params)
if @order.save
result = OrderProcessor.new(@order, payment_method: params[:payment_method]).call
if result.success?
redirect_to @order, notice: 'Pesanan dikonfirmasi!'
else
@order.update!(status: 'payment_failed')
flash.now[:alert] = result.error
render :new, status: :unprocessable_entity
end
else
render :new, status: :unprocessable_entity
end
end
endPola Service Object mengikuti konvensi sederhana: satu kelas, satu tanggung jawab, satu metode publik call. Mengembalikan objek Result memungkinkan penanganan keberhasilan dan kegagalan yang rapi.
Pertanyaan 9: Jelaskan Concerns di Rails
Concerns memungkinkan ekstraksi dan berbagi kode antara Model atau Controller. Concerns menggunakan ActiveSupport::Concern untuk sintaks include yang bersih.
# app/models/concerns/sluggable.rb
# Concern yang dapat digunakan ulang untuk menghasilkan slug
module Sluggable
extend ActiveSupport::Concern
included do
# Kode yang dijalankan saat di-include
before_validation :generate_slug, if: :should_generate_slug?
validates :slug, presence: true, uniqueness: true
end
# Metode kelas
class_methods do
def find_by_slug!(slug)
find_by!(slug: slug)
end
def sluggable_source(column = :title)
@sluggable_source = column
end
def sluggable_source_column
@sluggable_source || :title
end
end
# Metode instance
def to_param
slug
end
private
def should_generate_slug?
slug.blank? || send("#{self.class.sluggable_source_column}_changed?")
end
def generate_slug
source = send(self.class.sluggable_source_column)
return if source.blank?
base_slug = source.parameterize
self.slug = unique_slug(base_slug)
end
def unique_slug(base)
slug = base
counter = 1
while self.class.where(slug: slug).where.not(id: id).exists?
slug = "#{base}-#{counter}"
counter += 1
end
slug
end
end# app/models/article.rb
class Article < ApplicationRecord
include Sluggable
sluggable_source :title # Opsional, default :title
end
# app/models/product.rb
class Product < ApplicationRecord
include Sluggable
sluggable_source :name
end# app/controllers/concerns/pagination.rb
# Concern untuk controller
module Pagination
extend ActiveSupport::Concern
included do
helper_method :page_param, :per_page_param
end
private
def paginate(relation)
relation.page(page_param).per(per_page_param)
end
def page_param
params[:page]&.to_i || 1
end
def per_page_param
[params[:per_page]&.to_i || 25, 100].min
end
endConcerns berguna untuk kode yang benar-benar dibagikan. Hindari membuat Concern hanya untuk "memperpendek" sebuah Model: itu menyembunyikan kompleksitas tanpa menguranginya.
Pengujian dengan RSpec
Pertanyaan 10: Bagaimana menyusun tes RSpec di Rails?
RSpec adalah framework testing standar untuk Rails. Struktur tes yang baik mencakup Model spec, Controller spec, Service spec, dan tes integrasi.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
# Factory dengan FactoryBot
let(:user) { build(:user) }
let(:admin) { build(:user, :admin) }
describe 'validations' do
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
it 'memvalidasi format email' do
user.email = 'invalid'
expect(user).not_to be_valid
expect(user.errors[:email]).to include('is invalid')
end
end
describe 'associations' do
it { is_expected.to have_many(:articles).dependent(:destroy) }
it { is_expected.to have_one(:profile) }
it { is_expected.to belong_to(:organization).optional }
end
describe '#full_name' do
it 'mengembalikan nama depan dan belakang yang digabung' do
user = build(:user, first_name: 'John', last_name: 'Doe')
expect(user.full_name).to eq('John Doe')
end
it 'menangani nama belakang yang kosong' do
user = build(:user, first_name: 'John', last_name: nil)
expect(user.full_name).to eq('John')
end
end
describe '.active' do
it 'mengembalikan hanya pengguna aktif' do
active = create(:user, active: true)
inactive = create(:user, active: false)
expect(User.active).to include(active)
expect(User.active).not_to include(inactive)
end
end
end# spec/services/order_processor_spec.rb
require 'rails_helper'
RSpec.describe OrderProcessor do
let(:user) { create(:user) }
let(:product) { create(:product, stock_quantity: 10, price: 100) }
let(:order) { create(:order, user: user, items: [build(:order_item, product: product, quantity: 2)]) }
subject { described_class.new(order, payment_method: 'card') }
describe '#call' do
context 'ketika pesanan valid' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: true, transaction_id: 'txn_123')
)
end
it 'memproses pesanan dengan sukses' do
result = subject.call
expect(result).to be_success
expect(order.reload.status).to eq('completed')
end
it 'mengurangi stok produk' do
expect { subject.call }.to change { product.reload.stock_quantity }.by(-2)
end
it 'mengirim email konfirmasi' do
expect { subject.call }
.to have_enqueued_mail(OrderMailer, :confirmation)
.with(order)
end
end
context 'ketika pembayaran gagal' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: false, error: 'Card declined')
)
end
it 'mengembalikan hasil kegagalan' do
result = subject.call
expect(result).to be_failure
expect(result.error).to include('Card declined')
end
it 'tidak memperbarui status pesanan' do
expect { subject.call }.not_to change { order.reload.status }
end
end
end
end# spec/requests/api/v1/products_spec.rb
require 'rails_helper'
RSpec.describe 'API V1 Products', type: :request do
let(:user) { create(:user) }
let(:headers) { { 'Authorization' => "Bearer #{user.api_token}" } }
describe 'GET /api/v1/products' do
let!(:products) { create_list(:product, 3, :active) }
it 'mengembalikan daftar produk' do
get '/api/v1/products', headers: headers
expect(response).to have_http_status(:ok)
expect(json_response['data'].size).to eq(3)
end
it 'memfilter berdasarkan kategori' do
category = create(:category)
categorized = create(:product, category: category)
get '/api/v1/products', params: { category_id: category.id }, headers: headers
expect(json_response['data'].map { |p| p['id'] }).to eq([categorized.id])
end
end
describe 'POST /api/v1/products' do
let(:valid_params) do
{ product: { name: 'Produk Baru', price: 99.99, category_id: create(:category).id } }
end
it 'membuat produk baru' do
expect {
post '/api/v1/products', params: valid_params, headers: headers
}.to change(Product, :count).by(1)
expect(response).to have_http_status(:created)
end
end
endPraktik terbaik: gunakan let untuk data, describe untuk metode/konteks, context untuk kondisi, dan it untuk asersi spesifik. Satu tes harus menguji satu hal.
Pertanyaan 11: Bagaimana menggunakan factory dengan FactoryBot?
FactoryBot memungkinkan pembuatan data uji secara deklaratif dan mudah dipelihara. Factory menggantikan fixture statis.
# spec/factories/users.rb
FactoryBot.define do
factory :user do
# Sequence untuk memastikan keunikan
sequence(:email) { |n| "user#{n}@example.com" }
first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
password { 'password123' }
confirmed_at { Time.current }
# Trait untuk variasi
trait :admin do
role { 'admin' }
after(:create) do |user|
user.permissions.create!(name: 'admin_access')
end
end
trait :unconfirmed do
confirmed_at { nil }
end
trait :with_profile do
after(:create) do |user|
create(:profile, user: user)
end
end
trait :with_articles do
transient do
articles_count { 3 }
end
after(:create) do |user, evaluator|
create_list(:article, evaluator.articles_count, author: user)
end
end
# Factory turunan
factory :admin_user do
admin
with_profile
end
end
end# spec/factories/orders.rb
FactoryBot.define do
factory :order do
user
status { 'pending' }
trait :with_items do
transient do
items_count { 2 }
end
after(:create) do |order, evaluator|
create_list(:order_item, evaluator.items_count, order: order)
order.recalculate_total!
end
end
trait :completed do
status { 'completed' }
processed_at { Time.current }
with_items
end
trait :high_value do
after(:create) do |order|
create(:order_item, order: order, quantity: 10, unit_price: 500)
order.recalculate_total!
end
end
end
end# Penggunaan dalam tes
RSpec.describe OrderProcessor do
# build: instance tidak persisten
let(:user) { build(:user) }
# create: persisten ke DB
let(:order) { create(:order, :with_items, user: user) }
# create_list: beberapa instance
let(:products) { create_list(:product, 5) }
# Menggabungkan trait
let(:admin) { create(:user, :admin, :with_profile) }
# Mengganti atribut
let(:expensive_order) { create(:order, :with_items, items_count: 10) }
# build_stubbed: lebih cepat, untuk unit test
let(:stubbed_user) { build_stubbed(:user) }
endLebih baik menggunakan build atau build_stubbed daripada create ketika persistensi tidak diperlukan: ini mempercepat tes secara signifikan.
Background Job
Pertanyaan 12: Bagaimana menggunakan Active Job dan Sidekiq di Rails?
Active Job menyediakan antarmuka terpadu untuk pekerjaan latar belakang, terlepas dari backend (Sidekiq, Resque, dll.). Sidekiq adalah pilihan populer karena performanya dengan Redis.
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
queue_as :default
# Konfigurasi retry
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10
discard_on ActiveJob::DeserializationError
# Opsi Sidekiq (jika backend Sidekiq)
sidekiq_options retry: 5, backtrace: true
def perform(order_id)
order = Order.find(order_id)
OrderProcessor.new(order).call
rescue ActiveRecord::RecordNotFound
# Pesanan dihapus antara antrean dan eksekusi
Rails.logger.warn("Order #{order_id} not found, skipping job")
end
end# app/jobs/batch_email_job.rb
class BatchEmailJob < ApplicationJob
queue_as :mailers
# Pembatasan laju dengan Sidekiq Enterprise atau gem throttle
sidekiq_options throttle: { threshold: 100, period: 1.minute }
def perform(user_ids, template_id)
template = EmailTemplate.find(template_id)
User.where(id: user_ids).find_each do |user|
UserMailer.custom_email(user, template).deliver_later
end
end
end# Mengantrekan pekerjaan
# Segera
ProcessOrderJob.perform_later(order.id)
# Tertunda
ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id)
# Pada waktu spesifik
ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id)
# Antrean spesifik
ProcessOrderJob.set(queue: :critical).perform_later(order.id)
# Sinkron (untuk tes atau debugging)
ProcessOrderJob.perform_now(order.id)# config/sidekiq.yml
:concurrency: 10
:queues:
- [critical, 3] # Prioritas tinggi, bobot 3
- [default, 2] # Prioritas sedang, bobot 2
- [mailers, 1] # Prioritas rendah, bobot 1
- [low, 1]
:schedule:
cleanup_job:
cron: '0 3 * * *' # Setiap hari pukul 3 pagi
class: CleanupJobActive Job mengabstraksi backend, tetapi mengakses fitur spesifik (batch, rate limiting) sering memerlukan keterikatan dengan backend yang dipilih.
Siap menguasai wawancara Ruby on Rails Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Pengembangan API
Pertanyaan 13: Bagaimana membangun RESTful API dengan Rails?
Rails memudahkan pembangunan API JSON dengan controller API-only dan serializer. API yang baik memiliki versi, terdokumentasi, dan aman.
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate_token!
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def authenticate_token!
authenticate_or_request_with_http_token do |token, options|
@current_user = User.find_by(api_token: token)
end
end
def current_user
@current_user
end
def not_found(exception)
render json: { error: 'Sumber daya tidak ditemukan', details: exception.message },
status: :not_found
end
def unprocessable_entity(exception)
render json: { error: 'Validasi gagal', details: exception.record.errors },
status: :unprocessable_entity
end
def bad_request(exception)
render json: { error: 'Permintaan tidak valid', details: exception.message },
status: :bad_request
end
end
end
end# app/controllers/api/v1/products_controller.rb
module Api
module V1
class ProductsController < BaseController
before_action :set_product, only: [:show, :update, :destroy]
def index
@products = Product.active
.includes(:category)
.page(params[:page])
.per(params[:per_page] || 20)
render json: {
data: ProductSerializer.new(@products).serializable_hash,
meta: pagination_meta(@products)
}
end
def show
render json: ProductSerializer.new(@product, include: [:category, :reviews])
end
def create
@product = Product.new(product_params)
@product.save!
render json: ProductSerializer.new(@product), status: :created
end
def update
@product.update!(product_params)
render json: ProductSerializer.new(@product)
end
def destroy
@product.destroy!
head :no_content
end
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.require(:product).permit(:name, :description, :price, :category_id)
end
def pagination_meta(collection)
{
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count
}
end
end
end
end# app/serializers/product_serializer.rb
# Dengan gem jsonapi-serializer
class ProductSerializer
include JSONAPI::Serializer
attributes :id, :name, :description, :price, :created_at
attribute :formatted_price do |product|
"$#{product.price.to_f.round(2)}"
end
belongs_to :category
has_many :reviews
link :self do |product|
Rails.application.routes.url_helpers.api_v1_product_url(product)
end
endPraktik terbaik API: lakukan versi dengan namespace, gunakan kode HTTP yang sesuai, paginasi koleksi, dan berikan pesan kesalahan yang jelas.
Pertanyaan 14: Bagaimana mengimplementasikan autentikasi JWT di Rails?
JWT (JSON Web Tokens) adalah metode autentikasi stateless yang populer untuk API. Token mengkodekan identitas dan validitas pengguna.
# app/services/jwt_service.rb
class JwtService
SECRET_KEY = Rails.application.credentials.secret_key_base
ALGORITHM = 'HS256'.freeze
class << self
def encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
payload[:iat] = Time.current.to_i
JWT.encode(payload, SECRET_KEY, ALGORITHM)
end
def decode(token)
decoded = JWT.decode(token, SECRET_KEY, true, algorithm: ALGORITHM)
HashWithIndifferentAccess.new(decoded.first)
rescue JWT::ExpiredSignature
raise AuthenticationError, 'Token kedaluwarsa'
rescue JWT::DecodeError
raise AuthenticationError, 'Token tidak valid'
end
end
end# app/controllers/api/v1/auth_controller.rb
module Api
module V1
class AuthController < ActionController::API
def login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
token = JwtService.encode(user_id: user.id)
render json: {
token: token,
user: UserSerializer.new(user),
expires_at: 24.hours.from_now
}
else
render json: { error: 'Kredensial tidak valid' }, status: :unauthorized
end
end
def refresh
token = JwtService.encode(user_id: current_user.id)
render json: { token: token, expires_at: 24.hours.from_now }
end
end
end
end# app/controllers/concerns/jwt_authenticatable.rb
module JwtAuthenticatable
extend ActiveSupport::Concern
included do
before_action :authenticate_jwt!
end
private
def authenticate_jwt!
header = request.headers['Authorization']
token = header&.split(' ')&.last
raise AuthenticationError, 'Token tidak ada' unless token
decoded = JwtService.decode(token)
@current_user = User.find(decoded[:user_id])
rescue AuthenticationError => e
render json: { error: e.message }, status: :unauthorized
rescue ActiveRecord::RecordNotFound
render json: { error: 'Pengguna tidak ditemukan' }, status: :unauthorized
end
def current_user
@current_user
end
endUntuk produksi, pertimbangkan: refresh token, blacklist token saat logout, dan waktu kedaluwarsa singkat. Gem seperti devise-jwt mempermudah implementasi.
Caching dan performa
Pertanyaan 15: Bagaimana mengimplementasikan caching di Rails?
Rails menawarkan beberapa tingkat caching: fragment caching, Russian Doll caching, low-level caching. Pilihannya bergantung pada kasus penggunaan.
# config/environments/production.rb
config.action_controller.perform_caching = true
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
namespace: 'myapp:cache',
expires_in: 1.day,
race_condition_ttl: 10.seconds
}<%# app/views/products/index.html.erb %>
<%# Fragment caching dengan kunci cache otomatis %>
<% @products.each do |product| %>
<%# Cache berdasarkan updated_at produk %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
<%# Russian Doll caching - cache bersarang %>
<% cache ['v1', @category] do %>
<h2><%= @category.name %></h2>
<% @category.products.each do |product| %>
<% cache ['v1', product] do %>
<%= render product %>
<% end %>
<% end %>
<% end %>
<%# Cache bersyarat %>
<% cache_if current_user.nil?, @product do %>
<%= render @product %>
<% end %># app/models/product.rb
class Product < ApplicationRecord
# Touch parent untuk membatalkan Russian Doll cache
belongs_to :category, touch: true
# Kunci cache kustom
def cache_key_with_version
"#{super}/#{reviews.maximum(:updated_at)&.to_i}"
end
end# Low-level caching di service
class DashboardStatsService
def call
Rails.cache.fetch('dashboard:stats', expires_in: 15.minutes) do
{
total_users: User.count,
active_users: User.where('last_sign_in_at > ?', 30.days.ago).count,
total_orders: Order.completed.count,
revenue_mtd: Order.completed.where(created_at: Time.current.beginning_of_month..).sum(:total)
}
end
end
end
# Cache dengan perlindungan terhadap race condition
Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do
Product.bestsellers.limit(10).to_a
end
# Pembatalan eksplisit
Rails.cache.delete('dashboard:stats')
Rails.cache.delete_matched('products:*')Russian Doll caching efektif karena hanya fragmen yang dimodifikasi yang diregenerasi. Gunakan touch: true pada asosiasi untuk menyebarkan invalidasi.
Pertanyaan 16: Bagaimana mengoptimalkan performa aplikasi Rails?
Optimasi Rails meliputi banyak aspek: query DB, caching, aset, dan arsitektur. Pendekatan metodis dengan monitoring sangat penting.
# Optimasi basis data
# config/database.yml
production:
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
prepared_statements: true
advisory_locks: true
# app/models/order.rb
class Order < ApplicationRecord
# Indeks komposit untuk query yang sering
# add_index :orders, [:user_id, :status, :created_at]
# Pilih hanya kolom yang diperlukan
scope :summary, -> { select(:id, :status, :total, :created_at) }
# Pemrosesan batch untuk volume besar
def self.process_pending
pending.find_each(batch_size: 1000) do |order|
ProcessOrderJob.perform_later(order.id)
end
end
# Hindari perhitungan yang berulang
def self.revenue_by_month
completed
.group("DATE_TRUNC('month', created_at)")
.sum(:total)
end
end# Optimasi memori
# config/puma.rb
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
preload_app!
before_fork do
ActiveRecord::Base.connection_pool.disconnect!
end
on_worker_boot do
ActiveRecord::Base.establish_connection
end# Profiling dengan rack-mini-profiler
# Gemfile
group :development do
gem 'rack-mini-profiler'
gem 'memory_profiler'
gem 'stackprof'
end
# config/initializers/mini_profiler.rb
if defined?(Rack::MiniProfiler)
Rack::MiniProfiler.config.position = 'bottom-right'
Rack::MiniProfiler.config.start_hidden = true
end# Lazy loading dan paginasi
class ProductsController < ApplicationController
def index
@products = Product.active
.includes(:category, :primary_image)
.page(params[:page])
.per(24)
# Prefetch untuk halaman berikutnya
if @products.next_page
Rails.cache.fetch("products:page:#{@products.next_page}", expires_in: 5.minutes) do
Product.active.page(@products.next_page).per(24).to_a
end
end
end
endAlat penting: rack-mini-profiler untuk profiling, bullet untuk deteksi N+1, New Relic atau Scout untuk monitoring produksi.
Keamanan
Pertanyaan 17: Apa praktik terbaik keamanan di Rails?
Rails menyertakan perlindungan default terhadap kerentanan umum. Memahami dan mengonfigurasi perlindungan ini dengan benar sangat penting.
# Perlindungan CSRF
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Aktif secara default, melontarkan exception jika token tidak valid
protect_from_forgery with: :exception
# Untuk API gunakan :null_session
# protect_from_forgery with: :null_session
end
# Pada view, token disertakan otomatis di formulir
# <%= form_with ... %> menyertakan authenticity_token
# Untuk permintaan AJAX
# Tambahkan header X-CSRF-Token dengan nilai csrf_meta_tags# Pencegahan SQL Injection
# ✅ Parameter yang diinterpolasi otomatis di-escape
User.where('email = ?', params[:email])
User.where(email: params[:email])
# ❌ BAHAYA - Interpolasi langsung
User.where("email = '#{params[:email]}'")
# ✅ Untuk klausa ORDER dinamis
ALLOWED_SORTS = %w[name created_at price].freeze
sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name'
Product.order(sort_column)# Perlindungan XSS
# Rails secara otomatis meng-escape HTML di view
# ✅ Otomatis di-escape
<%= user.name %>
# ❌ Berbahaya - konten tanpa escape
<%== user.bio %>
<%= raw user.bio %>
<%= user.bio.html_safe %>
# ✅ Untuk HTML aman, gunakan sanitize
<%= sanitize user.bio, tags: %w[p br strong em] %># Strong Parameters
class UsersController < ApplicationController
def update
@user.update!(user_params)
end
private
def user_params
# Whitelist eksplisit atribut yang diizinkan
params.require(:user).permit(:name, :email, :avatar)
# Hanya untuk admin
if current_user.admin?
params.require(:user).permit(:name, :email, :role, :active)
else
params.require(:user).permit(:name, :email)
end
end
end# Header keamanan
# config/initializers/secure_headers.rb
Rails.application.config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
'X-Content-Type-Options' => 'nosniff',
'X-Download-Options' => 'noopen',
'X-Permitted-Cross-Domain-Policies' => 'none',
'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
# Content Security Policy
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self
policy.style_src :self, :unsafe_inline
policy.img_src :self, :data, 'https:'
endAudit secara berkala dengan brakeman (analisis keamanan statis) dan jaga gem tetap up-to-date dengan bundle audit.
Pertanyaan 18: Bagaimana menangani autentikasi dan otorisasi di Rails?
Autentikasi memverifikasi identitas, otorisasi mengontrol izin. Devise menangani autentikasi, Pundit atau CanCanCan menangani otorisasi.
# Setup Devise
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:confirmable, :lockable, :trackable
enum role: { user: 0, moderator: 1, admin: 2 }
def admin?
role == 'admin'
end
end# Kebijakan Pundit
# app/policies/article_policy.rb
class ArticlePolicy < ApplicationPolicy
def index?
true
end
def show?
record.published? || owner_or_admin?
end
def create?
user.present?
end
def update?
owner_or_admin?
end
def destroy?
owner_or_admin?
end
def publish?
user&.admin? || user&.moderator?
end
# Scope untuk koleksi
class Scope < Scope
def resolve
if user&.admin?
scope.all
elsif user
scope.where(published: true).or(scope.where(author: user))
else
scope.where(published: true)
end
end
end
private
def owner_or_admin?
user&.admin? || record.author == user
end
end# Controller dengan Pundit
class ArticlesController < ApplicationController
include Pundit::Authorization
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
def index
@articles = policy_scope(Article).includes(:author).page(params[:page])
end
def show
@article = Article.find(params[:id])
authorize @article
end
def update
@article = Article.find(params[:id])
authorize @article
if @article.update(article_params)
redirect_to @article, notice: 'Artikel diperbarui.'
else
render :edit, status: :unprocessable_entity
end
end
def publish
@article = Article.find(params[:id])
authorize @article
@article.update!(published: true, published_at: Time.current)
redirect_to @article, notice: 'Artikel dipublikasikan.'
end
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "Anda tidak diizinkan melakukan tindakan ini."
redirect_back(fallback_location: root_path)
end
endPundit lebih eksplisit dan dapat diuji daripada CanCanCan. Setiap aksi memiliki metode policy yang sesuai, dan scope memfilter koleksi secara otomatis.
Rails tingkat lanjut
Pertanyaan 19: Jelaskan pola Repository di Rails
Pola Repository mengisolasi logika akses data dari sisa aplikasi. Meskipun Rails menggunakan Active Record (pola berbeda), Repository dapat berguna untuk kasus kompleks.
# app/repositories/base_repository.rb
class BaseRepository
def initialize(model_class)
@model_class = model_class
end
def all
@model_class.all
end
def find(id)
@model_class.find(id)
end
def find_by(attributes)
@model_class.find_by(attributes)
end
def create(attributes)
@model_class.create(attributes)
end
def update(record, attributes)
record.update(attributes)
end
def delete(record)
record.destroy
end
end# app/repositories/product_repository.rb
class ProductRepository < BaseRepository
def initialize
super(Product)
end
def active
@model_class.where(active: true)
end
def in_category(category_id)
@model_class.where(category_id: category_id)
end
def search(query)
@model_class.where('name ILIKE ? OR description ILIKE ?',
"%#{query}%", "%#{query}%")
end
def with_stock
@model_class.where('stock_quantity > 0')
end
def bestsellers(limit: 10)
@model_class
.joins(:order_items)
.group(:id)
.order('COUNT(order_items.id) DESC')
.limit(limit)
end
def for_homepage
active
.with_stock
.includes(:category, :primary_image)
.order(featured: :desc, created_at: :desc)
.limit(12)
end
end# Penggunaan dalam service
class ProductSearchService
def initialize(repository: ProductRepository.new)
@repository = repository
end
def call(params)
products = @repository.active
products = products.in_category(params[:category]) if params[:category]
products = products.search(params[:query]) if params[:query].present?
products = products.with_stock if params[:in_stock]
products
end
end
# Memudahkan pengujian dengan mock
RSpec.describe ProductSearchService do
let(:repository) { instance_double(ProductRepository) }
let(:service) { described_class.new(repository: repository) }
it 'memfilter berdasarkan kategori' do
products = double('products')
allow(repository).to receive(:active).and_return(products)
allow(products).to receive(:in_category).with(1).and_return(products)
service.call(category: 1)
expect(products).to have_received(:in_category).with(1)
end
endRepository opsional di Rails karena Active Record sudah merupakan pola yang sangat baik. Gunakan untuk query kompleks atau ketika isolasi penyimpanan penting.
Pertanyaan 20: Bagaimana mengimplementasikan pola CQRS di Rails?
CQRS (Command Query Responsibility Segregation) memisahkan operasi baca dan tulis. Di Rails, ini berarti kelas terpisah untuk query dan command.
# app/commands/base_command.rb
class BaseCommand
include ActiveModel::Validations
def self.call(*args)
new(*args).call
end
def call
return failure(errors) unless valid?
execute
end
private
def execute
raise NotImplementedError
end
def success(data = nil)
CommandResult.success(data)
end
def failure(errors)
CommandResult.failure(errors)
end
end
CommandResult = Struct.new(:success, :data, :errors, keyword_init: true) do
def success? = success
def failure? = !success
def self.success(data)
new(success: true, data: data, errors: [])
end
def self.failure(errors)
new(success: false, data: nil, errors: Array(errors))
end
end# app/commands/orders/create_order_command.rb
module Orders
class CreateOrderCommand < BaseCommand
attr_reader :user, :items, :shipping_address
validates :user, presence: true
validates :items, presence: true
validate :validate_items_availability
def initialize(user:, items:, shipping_address:)
@user = user
@items = items
@shipping_address = shipping_address
end
private
def execute
order = nil
ActiveRecord::Base.transaction do
order = Order.create!(
user: user,
shipping_address: shipping_address,
status: 'pending'
)
items.each do |item|
order.items.create!(
product_id: item[:product_id],
quantity: item[:quantity],
unit_price: Product.find(item[:product_id]).price
)
end
order.calculate_total!
end
OrderCreatedEvent.broadcast(order)
success(order)
rescue ActiveRecord::RecordInvalid => e
failure(e.message)
end
def validate_items_availability
items.each do |item|
product = Product.find_by(id: item[:product_id])
unless product&.stock_quantity&.>= item[:quantity]
errors.add(:items, "Produk #{item[:product_id]} tidak tersedia")
end
end
end
end
end# app/queries/orders/user_orders_query.rb
module Orders
class UserOrdersQuery
def initialize(user, params = {})
@user = user
@params = params
end
def call
orders = @user.orders.includes(:items, items: :product)
orders = apply_status_filter(orders)
orders = apply_date_filter(orders)
orders = apply_sorting(orders)
orders.page(@params[:page]).per(@params[:per_page] || 20)
end
private
def apply_status_filter(orders)
return orders unless @params[:status]
orders.where(status: @params[:status])
end
def apply_date_filter(orders)
orders = orders.where('created_at >= ?', @params[:from]) if @params[:from]
orders = orders.where('created_at <= ?', @params[:to]) if @params[:to]
orders
end
def apply_sorting(orders)
case @params[:sort]
when 'oldest' then orders.order(created_at: :asc)
when 'total_desc' then orders.order(total: :desc)
else orders.order(created_at: :desc)
end
end
end
end# Controller menggunakan CQRS
class OrdersController < ApplicationController
def index
@orders = Orders::UserOrdersQuery.new(current_user, filter_params).call
end
def create
result = Orders::CreateOrderCommand.call(
user: current_user,
items: order_params[:items],
shipping_address: order_params[:shipping_address]
)
if result.success?
redirect_to result.data, notice: 'Pesanan dibuat!'
else
flash.now[:alert] = result.errors.join(', ')
render :new, status: :unprocessable_entity
end
end
endCQRS bersinar pada aplikasi kompleks dengan kebutuhan baca/tulis yang asimetris. Untuk CRUD sederhana, ini adalah over-engineering.
Pertanyaan 21: Bagaimana menangani WebSocket dengan Action Cable?
Action Cable mengintegrasikan WebSocket ke dalam Rails untuk komunikasi dua arah secara real-time. Action Cable menggunakan Redis untuk sinkronisasi antar server.
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
# Melalui cookie sesi
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
# Melalui JWT untuk API
elsif verified_user = verify_jwt_token
verified_user
else
reject_unauthorized_connection
end
end
def verify_jwt_token
token = request.params[:token]
return nil unless token
decoded = JwtService.decode(token)
User.find(decoded[:user_id])
rescue
nil
end
end
end# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
@room = ChatRoom.find(params[:room_id])
# Periksa izin
unless @room.accessible_by?(current_user)
reject
return
end
stream_for @room
# Beri tahu yang lain tentang kehadiran
broadcast_presence(:joined)
end
def unsubscribed
broadcast_presence(:left) if @room
end
def send_message(data)
message = @room.messages.create!(
user: current_user,
content: data['content']
)
# Siarkan ke semua subscriber
ChatChannel.broadcast_to(@room, {
type: 'message',
message: MessageSerializer.new(message).as_json
})
end
def typing
ChatChannel.broadcast_to(@room, {
type: 'typing',
user: current_user.name
})
end
private
def broadcast_presence(action)
ChatChannel.broadcast_to(@room, {
type: 'presence',
action: action,
user: current_user.name,
online_count: @room.online_users_count
})
end
endimport consumer from "./consumer"
const chatChannel = consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: roomId },
{
connected() {
console.log("Connected to chat")
},
disconnected() {
console.log("Disconnected from chat")
},
received(data) {
switch(data.type) {
case 'message':
this.appendMessage(data.message)
break
case 'typing':
this.showTypingIndicator(data.user)
break
case 'presence':
this.updatePresence(data)
break
}
},
sendMessage(content) {
this.perform('send_message', { content: content })
},
notifyTyping() {
this.perform('typing')
}
}
)Action Cable secara otomatis menangani koneksi ulang dan sinkronisasi. Di produksi, konfigurasikan Redis sebagai adapter dan skalakan sesuai dengan koneksi bersamaan.
Pertanyaan 22: Bagaimana mengimplementasikan multi-tenancy di Rails?
Multi-tenancy memungkinkan aplikasi melayani beberapa pelanggan (tenant) yang terisolasi. Tiga pendekatan utama: tingkat database, skema, atau baris.
# Multitenancy tingkat baris dengan ActsAsTenant atau manual
# app/models/concerns/tenant_scoped.rb
module TenantScoped
extend ActiveSupport::Concern
included do
belongs_to :tenant
# Scope default ke tenant saat ini
default_scope -> { where(tenant: Current.tenant) if Current.tenant }
# Validasi tenant
before_validation :set_tenant, on: :create
end
private
def set_tenant
self.tenant ||= Current.tenant
end
end
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :tenant, :user
end# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_current_tenant
private
def set_current_tenant
Current.tenant = resolve_tenant
Current.user = current_user
end
def resolve_tenant
# Melalui subdomain
if request.subdomain.present? && request.subdomain != 'www'
Tenant.find_by!(subdomain: request.subdomain)
# Melalui header (untuk API)
elsif request.headers['X-Tenant-ID'].present?
Tenant.find(request.headers['X-Tenant-ID'])
# Melalui pengguna
elsif current_user
current_user.tenant
end
rescue ActiveRecord::RecordNotFound
redirect_to root_url(subdomain: 'www'), alert: 'Tenant tidak ditemukan'
end
end# app/models/project.rb
class Project < ApplicationRecord
include TenantScoped
has_many :tasks
belongs_to :owner, class_name: 'User'
end
# app/models/user.rb
class User < ApplicationRecord
include TenantScoped
has_many :projects, foreign_key: :owner_id
# Admin dapat menjadi anggota beberapa tenant
has_many :tenant_memberships
has_many :accessible_tenants, through: :tenant_memberships, source: :tenant
end# Tingkat skema dengan gem Apartment (PostgreSQL)
# config/initializers/apartment.rb
Apartment.configure do |config|
config.excluded_models = %w[Tenant User]
config.tenant_names = -> { Tenant.pluck(:subdomain) }
end
# Penggunaan
Apartment::Tenant.switch('acme') do
# Semua query dalam blok ini menggunakan skema 'acme'
Project.all # SELECT * FROM acme.projects
endTingkat baris paling sederhana tetapi membutuhkan perhatian terus-menerus terhadap kebocoran. Tingkat skema memberikan isolasi lebih baik tetapi memperumit migrasi. Pilih sesuai kebutuhan keamanan dan skalabilitas.
Pertanyaan 23: Bagaimana menyiapkan arsitektur microservices dengan Rails?
Rails dapat menjadi dasar arsitektur microservices dengan komunikasi melalui HTTP/gRPC atau antrean pesan. Kuncinya adalah mendefinisikan batas dengan baik.
# Klien layanan HTTP
# app/services/payment_service_client.rb
class PaymentServiceClient
include HTTParty
base_uri ENV.fetch('PAYMENT_SERVICE_URL')
def initialize
@options = {
headers: {
'Content-Type' => 'application/json',
'X-Service-Token' => ENV.fetch('SERVICE_TOKEN')
},
timeout: 10
}
end
def create_charge(amount:, currency:, source:, metadata: {})
response = self.class.post('/charges', @options.merge(
body: { amount: amount, currency: currency, source: source, metadata: metadata }.to_json
))
handle_response(response)
end
def get_charge(charge_id)
response = self.class.get("/charges/#{charge_id}", @options)
handle_response(response)
end
private
def handle_response(response)
case response.code
when 200..299
ServiceResult.success(response.parsed_response)
when 400..499
ServiceResult.failure(response.parsed_response['error'], code: response.code)
else
ServiceResult.failure('Layanan tidak tersedia', code: response.code)
end
rescue Net::OpenTimeout, Net::ReadTimeout
ServiceResult.failure('Timeout layanan')
end
end# Komunikasi event-driven dengan Sidekiq/Redis
# app/events/order_events.rb
module OrderEvents
class Created
include Wisper::Publisher
def call(order)
broadcast(:order_created, order)
end
end
end
# app/listeners/inventory_listener.rb
class InventoryListener
def order_created(order)
order.items.each do |item|
InventoryServiceClient.new.reserve_stock(
product_id: item.product_id,
quantity: item.quantity,
reference: order.id
)
end
end
end
# config/initializers/wisper.rb
Wisper.subscribe(InventoryListener.new, async: true)
Wisper.subscribe(NotificationListener.new, async: true)# Pola API Gateway
# app/controllers/api/v1/gateway_controller.rb
module Api
module V1
class GatewayController < BaseController
# Agregasikan beberapa layanan
def dashboard
results = Parallel.map([:orders, :inventory, :analytics], in_threads: 3) do |service|
fetch_from_service(service)
end
render json: {
orders: results[0],
inventory: results[1],
analytics: results[2]
}
end
private
def fetch_from_service(service)
case service
when :orders
OrderServiceClient.new.recent_orders(limit: 5)
when :inventory
InventoryServiceClient.new.low_stock_alerts
when :analytics
AnalyticsServiceClient.new.daily_summary
end
rescue => e
{ error: "#{service} tidak tersedia", message: e.message }
end
end
end
endUntuk microservices Rails: definisikan kontrak API yang jelas (OpenAPI), terapkan circuit breaker (gem circuitbox), dan gunakan tracing terdistribusi (gem opentelemetry).
Pertanyaan 24: Bagaimana men-deploy aplikasi Rails ke produksi?
Deployment Rails modern menggunakan kontainer atau PaaS. Konfigurasi produksi yang andal mencakup aset, basis data, dan monitoring.
# config/environments/production.rb
Rails.application.configure do
config.cache_classes = true
config.eager_load = true
config.consider_all_requests_local = false
# Aset
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
config.assets.compile = false
config.assets.digest = true
# Logging
config.log_level = ENV.fetch('LOG_LEVEL', 'info').to_sym
config.log_tags = [:request_id]
config.logger = ActiveSupport::Logger.new(STDOUT)
.tap { |logger| logger.formatter = Logger::Formatter.new }
.then { |logger| ActiveSupport::TaggedLogging.new(logger) }
# Cache
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
expires_in: 1.day
}
# Paksa SSL
config.force_ssl = true
config.ssl_options = { hsts: { subdomains: true } }
# Action Mailer
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV['SMTP_HOST'],
port: ENV['SMTP_PORT'],
user_name: ENV['SMTP_USER'],
password: ENV['SMTP_PASSWORD'],
authentication: :plain,
enable_starttls_auto: true
}
end# Dockerfile
FROM ruby:3.3-alpine AS builder
RUN apk add --no-cache build-base postgresql-dev nodejs yarn
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment true && \
bundle config set --local without 'development test' && \
bundle install
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN bundle exec rails assets:precompile
# Image produksi
FROM ruby:3.3-alpine
RUN apk add --no-cache postgresql-client tzdata
WORKDIR /app
COPY /app /app
COPY /usr/local/bundle /usr/local/bundle
ENV RAILS_ENV=production
ENV RAILS_LOG_TO_STDOUT=true
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]# docker-compose.production.yml
version: '3.8'
services:
web:
build: .
environment:
- DATABASE_URL=postgres://user:pass@db/app_production
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
depends_on:
- db
- redis
deploy:
replicas: 3
resources:
limits:
memory: 512M
sidekiq:
build: .
command: bundle exec sidekiq
environment:
- DATABASE_URL=postgres://user:pass@db/app_production
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
deploy:
replicas: 2
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:Daftar periksa produksi: SSL wajib, secret melalui ENV, health check, backup DB otomatis, monitoring (APM + log + metrik), dan alerting yang dikonfigurasi.
Pertanyaan 25: Apa fitur baru di Rails 7+ yang perlu diketahui?
Rails 7+ membawa perubahan signifikan: Hotwire secara default, import maps, kredensial terenkripsi yang ditingkatkan, dan banyak optimasi.
# Hotwire - Turbo Frames
# app/views/articles/index.html.erb
<%= turbo_frame_tag "articles" do %>
<% @articles.each do |article| %>
<%= turbo_frame_tag dom_id(article) do %>
<%= render article %>
<% end %>
<% end %>
<%= link_to "Muat lebih banyak", articles_path(page: @page + 1),
data: { turbo_frame: "articles" } %>
<% end %>
# Turbo Streams untuk pembaruan real-time
# app/controllers/comments_controller.rb
def create
@comment = @article.comments.create!(comment_params.merge(user: current_user))
respond_to do |format|
format.turbo_stream
format.html { redirect_to @article }
end
end
# app/views/comments/create.turbo_stream.erb
<%= turbo_stream.append "comments", @comment %>
<%= turbo_stream.update "comments_count", @article.comments.count %>
<%= turbo_stream.replace "comment_form", partial: "comments/form", locals: { comment: Comment.new } %># Controller Stimulus
# app/javascript/controllers/search_controller.js
import { Controller } from "@hotwired/stimulus"
import { debounce } from "lodash-es"
export default class extends Controller {
static targets = ["input", "results"]
static values = { url: String }
connect() {
this.search = debounce(this.search.bind(this), 300)
}
async search() {
const query = this.inputTarget.value
if (query.length < 2) return
const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`)
this.resultsTarget.innerHTML = await response.text()
}
}# Import Maps (tanpa bundler JavaScript)
# config/importmap.rb
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
# Pin dari CDN
pin "lodash-es", to: "https://ga.jspm.io/npm:lodash-es@4.17.21/lodash.js"# Active Record Encryption (Rails 7+)
# app/models/user.rb
class User < ApplicationRecord
encrypts :email, deterministic: true # Memungkinkan pencarian
encrypts :phone_number # Non-deterministik secara default
encrypts :ssn, deterministic: true, downcase: true
end
# config/credentials.yml.enc
active_record_encryption:
primary_key: abc123...
deterministic_key: def456...
key_derivation_salt: ghi789...# Peningkatan antarmuka query
# Rails 7.1+
# Query asinkron
users = User.where(active: true).load_async
# Lanjutkan pemrosesan saat query berjalan
# Akses hasil dengan users.to_a
# Common Table Expressions (CTE)
User.with(
recent_orders: Order.where('created_at > ?', 30.days.ago)
).joins('JOIN recent_orders ON recent_orders.user_id = users.id')
# Deteksi inverse_of otomatis
class Author < ApplicationRecord
has_many :books # inverse_of terdeteksi otomatis
end
# Strict loading secara default (mencegah N+1)
class ApplicationRecord < ActiveRecord::Base
self.strict_loading_by_default = true
endRails 7+ mengutamakan kesederhanaan (tanpa Webpack secara default) dan HTML-over-the-wire dengan Hotwire. Pendekatan ini mengurangi kompleksitas JavaScript sambil menghadirkan pengalaman pengguna yang modern.
Kesimpulan
Wawancara Ruby on Rails menilai penguasaan seluruh framework dan pemahaman akan konvensinya. Poin penting yang perlu diingat:
✅ Dasar-dasar: MVC, Active Record, migrasi, validasi, dan asosiasi
✅ Arsitektur: Service Object, Concerns, Query Object, dan pola CQRS
✅ Performa: query N+1, caching (fragment, Russian Doll, low-level), eager loading
✅ Pengujian: RSpec, FactoryBot, request spec, dan praktik terbaik pengujian
✅ Keamanan: CSRF, SQL injection, XSS, Strong Parameters, dan autentikasi/otorisasi
✅ API: desain RESTful, JWT, serializer, dan pemberian versi
✅ Produksi: background job, WebSocket, deployment, dan monitoring
Filosofi Rails (Convention over Configuration, DRY, dan Rails Way) memandu semua keputusan arsitektur. Menguasai prinsip-prinsip ini dan mengetahui kapan harus menyimpang darinya menunjukkan keahlian yang solid.
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

ActiveRecord: Mengatasi Masalah Query N+1 di Ruby on Rails
Panduan lengkap untuk mendeteksi dan mengatasi query N+1 di Rails dengan ActiveRecord. Kuasai includes, preload, eager_load, dan alat deteksi otomatis.

Ruby on Rails 7: Hotwire dan Turbo untuk Aplikasi Reaktif
Panduan lengkap Hotwire dan Turbo di Rails 7. Membangun aplikasi reaktif tanpa JavaScript dengan Turbo Drive, Frames dan Streams.

Rails API Mode di 2026: RESTful API, Serialisasi JSON, dan Pertanyaan Interview
Panduan lengkap Rails 8 API Mode: rute RESTful, serialisasi Alba dan jsonapi-serializer, autentikasi JWT, error handling, dan RSpec.