# 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. - Published: 2026-02-03 - Updated: 2026-04-27 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, ruby, wawancara, active record, wawancara teknis - Reading time: 22 min --- 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. > **Tips wawancara** > > 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. ```ruby # 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 ``` ```ruby # 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 ``` ```erb <%# app/views/articles/show.html.erb %> <%# View menampilkan data dalam format HTML %>

<%= @article.title %>

Oleh <%= @article.author.name %> • <%= l @article.created_at, format: :long %>

<%= simple_format @article.body %>
<%# Partial untuk komentar %> <%= render @comments %>
``` 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. ```ruby # 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 ``` ```ruby # 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) end ``` Active 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. ```ruby # 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 ``` ```ruby # 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 ``` ```bash # 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, seed ``` Migrasi 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 digunakan ``` Aturannya: 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. ```ruby # 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 ``` ```ruby # 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. ## 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. ```ruby # 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 ``` ```bash # 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#drafts ``` Helper 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. ```ruby # 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 ``` ```ruby # 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 end ``` Callback 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. ```ruby # 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 ``` ```ruby # 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 end ``` Pola 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Concerns 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Praktik 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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) } end ``` Lebih 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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) ``` ```ruby # 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: CleanupJob ``` Active Job mengabstraksi backend, tetapi mengakses fitur spesifik (batch, rate limiting) sering memerlukan keterikatan dengan backend yang dipilih. ## 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Praktik 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Untuk 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. ```ruby # 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 } ``` ```erb <%# 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 %>

<%= @category.name %>

<% @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 %> ``` ```ruby # 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 ``` ```ruby # 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Alat 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. ```ruby # 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 ``` ```ruby # 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) ``` ```ruby # 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] %> ``` ```ruby # 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 ``` ```ruby # 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:' end ``` Audit 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Pundit 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Repository 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` CQRS 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. ```ruby # 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 ``` ```ruby # 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 end ``` ```javascript // app/javascript/channels/chat_channel.js import 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Tingkat 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. ```ruby # 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 ``` ```ruby # 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) ``` ```ruby # 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 end ``` Untuk 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. ```ruby # 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 # 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 --from=builder /app /app COPY --from=builder /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"] ``` ```yaml # 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. ```ruby # 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 } %> ``` ```ruby # 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() } } ``` ```ruby # 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" ``` ```ruby # 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... ``` ```ruby # 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 end ``` Rails 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/ruby-on-rails/ruby-on-rails-interview-questions