Ruby on Rails Interview Questions: Top 25 in 2026
The 25 most asked Ruby on Rails interview questions. MVC architecture, Active Record, migrations, RSpec testing, REST APIs with detailed answers and code examples.

Ruby on Rails interviews assess mastery of Ruby's most popular framework, understanding of MVC architecture, Active Record ORM, and the ability to build robust web applications following the "Convention over Configuration" philosophy. This guide covers the 25 most asked questions, from Rails fundamentals to advanced production patterns.
Recruiters appreciate candidates who understand the Rails philosophy: "Convention over Configuration", DRY (Don't Repeat Yourself), and Rails Way patterns. Explaining why Rails makes certain architectural choices makes the difference.
Ruby on Rails Fundamentals
Question 1: Explain the MVC Pattern in Ruby on Rails
The Model-View-Controller (MVC) pattern is the architectural core of Rails. It separates responsibilities into three distinct layers for better maintainability and testability.
# app/models/article.rb
# The Model handles data and business logic
class Article < ApplicationRecord
# Data validations
validates :title, presence: true, length: { minimum: 5 }
validates :body, presence: true
# Associations with other models
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
has_many :tags, through: :article_tags
# Scopes for reusable queries
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
# Lifecycle callbacks
before_save :generate_slug
private
def generate_slug
self.slug = title.parameterize if title_changed?
end
end# app/controllers/articles_controller.rb
# The Controller receives requests and orchestrates the response
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: 'Article created successfully.'
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 %>
<%# The View displays data in HTML format %>
<article class="article-detail">
<header>
<h1><%= @article.title %></h1>
<p class="meta">
By <%= @article.author.name %> •
<%= l @article.created_at, format: :long %>
</p>
</header>
<div class="content">
<%= simple_format @article.body %>
</div>
<%# Partial for comments %>
<%= render @comments %>
</article>The typical flow: the request arrives at the Router, which dispatches to the appropriate Controller. The Controller interacts with the Model to retrieve or modify data, then passes this data to the View for HTML rendering.
Question 2: What is Active Record and How Does Rails ORM Work?
Active Record is Rails' ORM (Object-Relational Mapping) that implements the Active Record pattern. Each Model class represents a database table, and each instance represents a row.
# app/models/user.rb
# Active Record automatically maps columns to attributes
class User < ApplicationRecord
# The 'users' table is automatically associated
# Columns: id, email, name, created_at, updated_at
has_secure_password # BCrypt for password
has_many :articles, foreign_key: :author_id
has_one :profile, dependent: :destroy
has_and_belongs_to_many :roles
# Validations
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
# Callbacks
before_save :normalize_email
# Class methods for queries
def self.admins
joins(:roles).where(roles: { name: 'admin' })
end
private
def normalize_email
self.email = email.downcase.strip
end
end# Active Record query examples
# Rails console or within a service
# Creation
user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123')
# Reading with conditions
active_users = User.where(active: true).order(:name)
user = User.find_by(email: 'dev@example.com')
# Chained queries (lazy evaluation)
recent_admins = User.admins
.where('created_at > ?', 1.month.ago)
.includes(:profile)
.limit(10)
# N+1 prevention with eager loading
articles = Article.includes(:author, :comments).published
# Update
user.update!(name: 'Alice Martin')
# Transactions
User.transaction do
user.debit_balance!(100)
recipient.credit_balance!(100)
Payment.create!(from: user, to: recipient, amount: 100)
endActive Record converts Ruby methods into optimized SQL queries. Methods like where, joins, includes are lazy - the query is only executed when iterating or calling to_a.
Question 3: Explain the Rails Migration System
Migrations allow versioning database schema with Ruby. They are reversible and enable controlled evolution of data structure.
# db/migrate/20260203100000_create_products.rb
# Migration to create a table
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 and updated_at automatic
end
# Indexes for performance
add_index :products, :name
add_index :products, [:category_id, :active]
end
end# db/migrate/20260203110000_add_slug_to_products.rb
# Migration to modify an existing table
class AddSlugToProducts < ActiveRecord::Migration[7.1]
def change
add_column :products, :slug, :string
add_index :products, :slug, unique: true
# Fill existing slugs
reversible do |dir|
dir.up do
Product.find_each do |product|
product.update_column(:slug, product.name.parameterize)
end
end
end
# Make non-nullable after filling
change_column_null :products, :slug, false
end
end# Essential migration commands
rails db:migrate # Run pending migrations
rails db:rollback # Undo last migration
rails db:rollback STEP=3 # Undo last 3 migrations
rails db:migrate:status # See migration status
rails db:seed # Run db/seeds.rb
rails db:reset # Drop, create, migrate, seedMigrations must be reversible. The change method is smart and can automatically reverse common operations. For complex cases, use up and down separately.
Advanced Active Record
Question 4: How to Optimize N+1 Queries in Rails?
The N+1 problem occurs when an initial query is followed by N additional queries to load associations. Rails provides several eager loading methods to solve this issue.
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def index
# ❌ N+1 PROBLEM: 1 query + N queries per order
# @orders = Order.all
# In the view: order.user.name generates a query per order
# ✅ SOLUTION with includes (eager loading)
@orders = Order.includes(:user, :items)
.where(status: 'completed')
.order(created_at: :desc)
# Generates only 3 queries total
end
def show
# includes: loads associations separately (2-3 queries)
@order = Order.includes(items: :product).find(params[:id])
# preload: forces separate loading
@order = Order.preload(:items, :user).find(params[:id])
# eager_load: forces 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 with default includes
scope :with_details, -> { includes(:user, items: :product) }
# Counter cache to avoid COUNT queries
# Requires: add_column :users, :orders_count, :integer, default: 0
belongs_to :user, counter_cache: true
end# N+1 detection with Bullet gem (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 will show alerts when:
# - An N+1 query is detected
# - Unnecessary eager loading is present
# - A counter cache should be usedThe rule: use includes by default (Rails chooses the optimal strategy), preload when forcing separate queries, eager_load when filtering on associations.
Question 5: Explain Scopes and Query Objects in Rails
Scopes encapsulate reusable query conditions. For complex queries, Query Objects offer better organization and testability.
# app/models/product.rb
class Product < ApplicationRecord
# Simple scopes
scope :active, -> { where(active: true) }
scope :in_stock, -> { where('stock_quantity > 0') }
scope :featured, -> { where(featured: true) }
# Scopes with parameters
scope :cheaper_than, ->(price) { where('price < ?', price) }
scope :in_category, ->(category) { where(category: category) }
# Chainable scopes
scope :available, -> { active.in_stock }
# Scope with joins
scope :with_recent_orders, -> {
joins(:order_items)
.where('order_items.created_at > ?', 30.days.ago)
.distinct
}
# Scope with 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 for complex searches
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
# Usage in controller
@products = ProductsSearchQuery.new(Product.active).call(params)Scopes are perfect for simple, reusable conditions. Query Objects suit complex searches with multiple optional filters and composition logic.
Ready to ace your Ruby on Rails interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Routing and Controllers
Question 6: How Does RESTful Routing Work in Rails?
Rails encourages RESTful routes that map HTTP verbs to CRUD actions. The router translates URLs into specific controller calls.
# config/routes.rb
Rails.application.routes.draw do
# Standard RESTful routes (7 actions)
resources :articles do
# Nested routes
resources :comments, only: [:create, :destroy]
# Member routes (act on an instance)
member do
post :publish
delete :archive
end
# Collection routes (act on the collection)
collection do
get :drafts
get :search
end
end
# API routes with namespace
namespace :api do
namespace :v1 do
resources :products, only: [:index, :show, :create, :update] do
resources :reviews, shallow: true
end
end
end
# Custom route
get 'dashboard', to: 'dashboard#index'
# Route constraints
constraints(SubdomainConstraint.new) do
resources :admin_settings
end
# Root route
root 'home#index'
end# rails routes - Shows all generated routes
#
# 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#draftsGenerated route helpers (article_path(@article), new_article_path) allow referencing URLs dynamically and maintainably.
Question 7: Explain Callbacks and Filters in Controllers
Callbacks (before_action, after_action, around_action) allow executing code before, after, or around controller actions.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# CSRF protection enabled by default
protect_from_forgery with: :exception
# Global callback for authentication
before_action :authenticate_user!
# Global error handling
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def not_found
render json: { error: 'Resource not found' }, 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
# Callbacks with options
before_action :require_admin
before_action :set_product, only: [:show, :edit, :update, :destroy]
after_action :log_activity, only: [:create, :update, :destroy]
# Conditional callback
before_action :check_stock, only: [:update], if: :stock_changed?
def create
@product = Product.new(product_params)
if @product.save
redirect_to [:admin, @product], notice: 'Product created.'
else
render :new, status: :unprocessable_entity
end
end
def update
if @product.update(product_params)
redirect_to [:admin, @product], notice: 'Product updated.'
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
endCallbacks execute in declaration order. Use skip_before_action in subclasses to disable inherited callbacks. Avoid callbacks with too much business logic - prefer Service Objects.
Services and Architecture
Question 8: How to Implement Service Objects in Rails?
Service Objects encapsulate complex business logic that belongs neither in Models nor Controllers. They improve testability and follow the single responsibility principle.
# app/services/order_processor.rb
# Service Object with standardized interface
class OrderProcessor
def initialize(order, payment_method:)
@order = order
@payment_method = payment_method
end
def call
return failure('Order already processed') 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("Payment failed: #{e.message}")
rescue InsufficientStockError => e
failure("Stock insufficient: #{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: "Order ##{@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: 'Order confirmed!'
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
endThe Service Object pattern follows a simple convention: one class, one responsibility, one public call method. Returning a Result object enables clean success and failure handling.
Question 9: Explain Concerns in Rails
Concerns allow extracting and sharing code between Models or Controllers. They use ActiveSupport::Concern for clean inclusion syntax.
# app/models/concerns/sluggable.rb
# Reusable Concern for generating slugs
module Sluggable
extend ActiveSupport::Concern
included do
# Code executed on inclusion
before_validation :generate_slug, if: :should_generate_slug?
validates :slug, presence: true, uniqueness: true
end
# Class methods
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
# Instance methods
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 # Optional, :title by default
end
# app/models/product.rb
class Product < ApplicationRecord
include Sluggable
sluggable_source :name
end# app/controllers/concerns/pagination.rb
# Concern for controllers
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 are useful for truly shared code. Avoid creating Concerns just to "shorten" a Model - that hides complexity without reducing it.
Testing with RSpec
Question 10: How to Structure RSpec Tests in Rails?
RSpec is the standard testing framework for Rails. A good test structure includes Model specs, Controller specs, Service specs, and integration tests.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
# Factories with 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 'validates email format' 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 'returns first and last name combined' do
user = build(:user, first_name: 'John', last_name: 'Doe')
expect(user.full_name).to eq('John Doe')
end
it 'handles missing last name' do
user = build(:user, first_name: 'John', last_name: nil)
expect(user.full_name).to eq('John')
end
end
describe '.active' do
it 'returns only active users' 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 'when order is valid' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: true, transaction_id: 'txn_123')
)
end
it 'processes the order successfully' do
result = subject.call
expect(result).to be_success
expect(order.reload.status).to eq('completed')
end
it 'decrements product stock' do
expect { subject.call }.to change { product.reload.stock_quantity }.by(-2)
end
it 'sends confirmation email' do
expect { subject.call }
.to have_enqueued_mail(OrderMailer, :confirmation)
.with(order)
end
end
context 'when payment fails' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: false, error: 'Card declined')
)
end
it 'returns failure result' do
result = subject.call
expect(result).to be_failure
expect(result.error).to include('Card declined')
end
it 'does not update order status' 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 'returns list of products' do
get '/api/v1/products', headers: headers
expect(response).to have_http_status(:ok)
expect(json_response['data'].size).to eq(3)
end
it 'filters by category' 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: 'New Product', price: 99.99, category_id: create(:category).id } }
end
it 'creates a new product' 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
endBest practices: use let for data, describe for methods/contexts, context for conditions, and it for specific assertions. One test should test one thing.
Question 11: How to Use Factories with FactoryBot?
FactoryBot enables creating test data declaratively and maintainably. Factories replace static fixtures.
# spec/factories/users.rb
FactoryBot.define do
factory :user do
# Sequences for uniqueness
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 }
# Traits for variations
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
# Inherited factory
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# Usage in tests
RSpec.describe OrderProcessor do
# build: non-persisted instance
let(:user) { build(:user) }
# create: persisted to DB
let(:order) { create(:order, :with_items, user: user) }
# create_list: multiple instances
let(:products) { create_list(:product, 5) }
# Combining traits
let(:admin) { create(:user, :admin, :with_profile) }
# Override attributes
let(:expensive_order) { create(:order, :with_items, items_count: 10) }
# build_stubbed: faster, for unit tests
let(:stubbed_user) { build_stubbed(:user) }
endPrefer build or build_stubbed over create when persistence isn't necessary - this significantly speeds up tests.
Background Jobs
Question 12: How to Use Active Job and Sidekiq in Rails?
Active Job provides a unified interface for background jobs, regardless of backend (Sidekiq, Resque, etc.). Sidekiq is the popular choice for its performance with Redis.
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
queue_as :default
# Retry configuration
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10
discard_on ActiveJob::DeserializationError
# Sidekiq options (if Sidekiq backend)
sidekiq_options retry: 5, backtrace: true
def perform(order_id)
order = Order.find(order_id)
OrderProcessor.new(order).call
rescue ActiveRecord::RecordNotFound
# Order deleted between enqueue and execution
Rails.logger.warn("Order #{order_id} not found, skipping job")
end
end# app/jobs/batch_email_job.rb
class BatchEmailJob < ApplicationJob
queue_as :mailers
# Rate limiting with Sidekiq Enterprise or throttle gem
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# Enqueuing jobs
# Immediate
ProcessOrderJob.perform_later(order.id)
# Delayed
ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id)
# At specific time
ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id)
# Specific queue
ProcessOrderJob.set(queue: :critical).perform_later(order.id)
# Synchronous (for tests or debugging)
ProcessOrderJob.perform_now(order.id)# config/sidekiq.yml
:concurrency: 10
:queues:
- [critical, 3] # High priority, weight 3
- [default, 2] # Medium priority, weight 2
- [mailers, 1] # Low priority, weight 1
- [low, 1]
:schedule:
cleanup_job:
cron: '0 3 * * *' # Every day at 3am
class: CleanupJobActive Job abstracts the backend, but accessing specific features (batches, rate limiting) often requires coupling to the chosen backend.
Ready to ace your Ruby on Rails interviews?
Practice with our interactive simulators, flashcards, and technical tests.
API Development
Question 13: How to Build a RESTful API with Rails?
Rails facilitates building JSON APIs with API-only Controllers and serializers. A good API is versioned, documented, and secure.
# 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: 'Resource not found', details: exception.message },
status: :not_found
end
def unprocessable_entity(exception)
render json: { error: 'Validation failed', details: exception.record.errors },
status: :unprocessable_entity
end
def bad_request(exception)
render json: { error: 'Bad request', 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
# With jsonapi-serializer gem
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
endAPI best practices: version via namespace, use appropriate HTTP codes, paginate collections, and provide clear error messages.
Question 14: How to Implement JWT Authentication in Rails?
JWT (JSON Web Tokens) is a popular stateless authentication method for APIs. The token encodes user identity and validity.
# 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 has expired'
rescue JWT::DecodeError
raise AuthenticationError, 'Invalid token'
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: 'Invalid credentials' }, 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, 'Missing token' 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: 'User not found' }, status: :unauthorized
end
def current_user
@current_user
end
endFor production, consider: refresh tokens, token blacklisting for logout, and short expiration times. Gems like devise-jwt simplify implementation.
Caching and Performance
Question 15: How to Implement Caching in Rails?
Rails offers several caching levels: fragment caching, Russian Doll caching, low-level caching. The choice depends on the use case.
# 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 with automatic cache key %>
<% @products.each do |product| %>
<%# Cache based on product's updated_at %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
<%# Russian Doll caching - nested cache %>
<% cache ['v1', @category] do %>
<h2><%= @category.name %></h2>
<% @category.products.each do |product| %>
<% cache ['v1', product] do %>
<%= render product %>
<% end %>
<% end %>
<% end %>
<%# Conditional cache %>
<% cache_if current_user.nil?, @product do %>
<%= render @product %>
<% end %># app/models/product.rb
class Product < ApplicationRecord
# Touch parent to invalidate Russian Doll cache
belongs_to :category, touch: true
# Custom cache key
def cache_key_with_version
"#{super}/#{reviews.maximum(:updated_at)&.to_i}"
end
end# Low-level caching in services
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 with race condition protection
Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do
Product.bestsellers.limit(10).to_a
end
# Explicit invalidation
Rails.cache.delete('dashboard:stats')
Rails.cache.delete_matched('products:*')Russian Doll caching is effective because only modified fragments are regenerated. Use touch: true on associations to propagate invalidation.
Question 16: How to Optimize Rails Application Performance?
Rails optimization covers multiple aspects: DB queries, caching, assets, and architecture. A methodical approach with monitoring is essential.
# Database optimization
# config/database.yml
production:
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
prepared_statements: true
advisory_locks: true
# app/models/order.rb
class Order < ApplicationRecord
# Composite indexes for frequent queries
# add_index :orders, [:user_id, :status, :created_at]
# Select only needed columns
scope :summary, -> { select(:id, :status, :total, :created_at) }
# Batch processing for large volumes
def self.process_pending
pending.find_each(batch_size: 1000) do |order|
ProcessOrderJob.perform_later(order.id)
end
end
# Avoid repetitive calculations
def self.revenue_by_month
completed
.group("DATE_TRUNC('month', created_at)")
.sum(:total)
end
end# Memory optimization
# 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 with 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 and pagination
class ProductsController < ApplicationController
def index
@products = Product.active
.includes(:category, :primary_image)
.page(params[:page])
.per(24)
# Prefetch for next page
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
endEssential tools: rack-mini-profiler for profiling, bullet for N+1 detection, New Relic or Scout for production monitoring.
Security
Question 17: What Are Rails Security Best Practices?
Rails includes default protections against common vulnerabilities. Understanding and correctly configuring these protections is crucial.
# CSRF Protection
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Enabled by default, raises exception if token invalid
protect_from_forgery with: :exception
# For APIs, use :null_session
# protect_from_forgery with: :null_session
end
# In views, the token is automatically included in forms
# <%= form_with ... %> includes authenticity_token
# For AJAX requests
# Add X-CSRF-Token header with csrf_meta_tags value# SQL Injection Prevention
# ✅ Interpolated parameters automatically escaped
User.where('email = ?', params[:email])
User.where(email: params[:email])
# ❌ DANGER - Direct interpolation
User.where("email = '#{params[:email]}'")
# ✅ For dynamic ORDER clauses
ALLOWED_SORTS = %w[name created_at price].freeze
sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name'
Product.order(sort_column)# XSS Protection
# Rails automatically escapes HTML in views
# ✅ Automatically escaped
<%= user.name %>
# ❌ Dangerous - unescaped content
<%== user.bio %>
<%= raw user.bio %>
<%= user.bio.html_safe %>
# ✅ For safe HTML, use 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
# Explicit whitelist of allowed attributes
params.require(:user).permit(:name, :email, :avatar)
# For admins only
if current_user.admin?
params.require(:user).permit(:name, :email, :role, :active)
else
params.require(:user).permit(:name, :email)
end
end
end# Secure Headers
# 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:'
endRegularly audit with brakeman (static security analysis) and keep gems updated with bundle audit.
Question 18: How to Handle Authentication and Authorization in Rails?
Authentication verifies identity, authorization controls permissions. Devise handles auth, Pundit or CanCanCan handle authorization.
# Devise setup
# 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# Pundit policies
# 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 for collections
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 with 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: 'Article updated.'
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: 'Article published.'
end
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_back(fallback_location: root_path)
end
endPundit is more explicit and testable than CanCanCan. Each action has a corresponding policy method, and scopes automatically filter collections.
Advanced Rails
Question 19: Explain the Repository Pattern in Rails
The Repository pattern isolates data access logic from the rest of the application. While Rails uses Active Record (a different pattern), Repository can be useful for complex cases.
# 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# Usage in a 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
# Facilitates testing with mocks
RSpec.describe ProductSearchService do
let(:repository) { instance_double(ProductRepository) }
let(:service) { described_class.new(repository: repository) }
it 'filters by category' 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 is optional in Rails since Active Record is already an excellent pattern. Use it for complex queries or when storage isolation is important.
Question 20: How to Implement the CQRS Pattern in Rails?
CQRS (Command Query Responsibility Segregation) separates read and write operations. In Rails, this translates to distinct classes for queries and commands.
# 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, "Product #{item[:product_id]} not available")
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 using 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: 'Order created!'
else
flash.now[:alert] = result.errors.join(', ')
render :new, status: :unprocessable_entity
end
end
endCQRS shines for complex applications with asymmetric read/write needs. For simple CRUD, it's over-engineering.
Question 21: How to Handle WebSockets with Action Cable?
Action Cable integrates WebSockets into Rails for bidirectional real-time communication. It uses Redis for synchronization between servers.
# 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
# Via session cookie
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
# Via JWT for APIs
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])
# Check permissions
unless @room.accessible_by?(current_user)
reject
return
end
stream_for @room
# Notify others of presence
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']
)
# Broadcast to all subscribers
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 automatically handles reconnections and synchronization. In production, configure Redis as adapter and scale according to concurrent connections.
Question 22: How to Implement Multi-tenancy in Rails?
Multi-tenancy allows an application to serve multiple isolated clients (tenants). Three main approaches: database-level, schema-level, or row-level.
# Row-level multitenancy with ActsAsTenant or manual
# app/models/concerns/tenant_scoped.rb
module TenantScoped
extend ActiveSupport::Concern
included do
belongs_to :tenant
# Default scope to current tenant
default_scope -> { where(tenant: Current.tenant) if Current.tenant }
# Tenant validation
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
# Via subdomain
if request.subdomain.present? && request.subdomain != 'www'
Tenant.find_by!(subdomain: request.subdomain)
# Via header (for APIs)
elsif request.headers['X-Tenant-ID'].present?
Tenant.find(request.headers['X-Tenant-ID'])
# Via user
elsif current_user
current_user.tenant
end
rescue ActiveRecord::RecordNotFound
redirect_to root_url(subdomain: 'www'), alert: 'Tenant not found'
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
# Admins can belong to multiple tenants
has_many :tenant_memberships
has_many :accessible_tenants, through: :tenant_memberships, source: :tenant
end# Schema-level with Apartment gem (PostgreSQL)
# config/initializers/apartment.rb
Apartment.configure do |config|
config.excluded_models = %w[Tenant User]
config.tenant_names = -> { Tenant.pluck(:subdomain) }
end
# Usage
Apartment::Tenant.switch('acme') do
# All queries in this block use the 'acme' schema
Project.all # SELECT * FROM acme.projects
endRow-level is simplest but requires constant attention to leaks. Schema-level offers better isolation but complicates migrations. Choose based on security and scalability needs.
Question 23: How to Set Up a Microservices Architecture with Rails?
Rails can serve as a base for microservices architecture with communication via HTTP/gRPC or message queues. The key is defining boundaries well.
# HTTP service client
# 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('Service unavailable', code: response.code)
end
rescue Net::OpenTimeout, Net::ReadTimeout
ServiceResult.failure('Service timeout')
end
end# Event-driven communication with 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)# API Gateway pattern
# app/controllers/api/v1/gateway_controller.rb
module Api
module V1
class GatewayController < BaseController
# Aggregate multiple services
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} unavailable", message: e.message }
end
end
end
endFor Rails microservices: define clear API contracts (OpenAPI), implement circuit breakers (gem circuitbox), and use distributed tracing (gem opentelemetry).
Question 24: How to Deploy a Rails Application to Production?
Modern Rails deployment uses containers or PaaS. A robust production configuration covers assets, database, and monitoring.
# config/environments/production.rb
Rails.application.configure do
config.cache_classes = true
config.eager_load = true
config.consider_all_requests_local = false
# Assets
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
}
# Force 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
# Production image
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:Production checklist: mandatory SSL, secrets via ENV, health checks, automated DB backups, monitoring (APM + logs + metrics), and configured alerting.
Question 25: What Are the New Features in Rails 7+ to Know?
Rails 7+ brings significant changes: Hotwire by default, import maps, improved encrypted credentials, and many optimizations.
# 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 "Load more", articles_path(page: @page + 1),
data: { turbo_frame: "articles" } %>
<% end %>
# Turbo Streams for real-time updates
# 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 } %># Stimulus controllers
# 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 (without JavaScript bundler)
# 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"
# Pins from 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 # Allows searches
encrypts :phone_number # Non-deterministic by 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...# Query interface improvements
# Rails 7.1+
# Async queries
users = User.where(active: true).load_async
# Continue processing while query runs
# Access results with 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')
# Automatic inverse_of detection
class Author < ApplicationRecord
has_many :books # inverse_of automatically detected
end
# Strict loading by default (avoids N+1)
class ApplicationRecord < ActiveRecord::Base
self.strict_loading_by_default = true
endRails 7+ favors simplicity (no Webpack by default) and HTML-over-the-wire with Hotwire. This approach reduces JavaScript complexity while offering a modern user experience.
Conclusion
Ruby on Rails interviews assess mastery of the complete framework and understanding of its conventions. Key takeaways:
✅ Fundamentals: MVC, Active Record, migrations, validations, and associations
✅ Architecture: Service Objects, Concerns, Query Objects, and CQRS patterns
✅ Performance: N+1 queries, caching (fragment, Russian Doll, low-level), eager loading
✅ Testing: RSpec, FactoryBot, request specs, and testing best practices
✅ Security: CSRF, SQL injection, XSS, Strong Parameters, and authentication/authorization
✅ APIs: RESTful design, JWT, serializers, and versioning
✅ Production: Background jobs, WebSockets, deployment, and monitoring
The Rails philosophy - Convention over Configuration, DRY, and Rails Way - guides all architectural decisions. Mastering these principles and knowing when to deviate demonstrates solid expertise.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

ActiveRecord: Fixing N+1 Query Problems in Ruby on Rails
Complete guide to detecting and fixing N+1 query problems in Rails with ActiveRecord. Master includes, preload, eager_load and automated detection tools.

Ruby on Rails 7: Hotwire and Turbo for Reactive Applications
Complete guide to Hotwire and Turbo in Rails 7. Learn to build reactive applications without writing JavaScript using Turbo Drive, Frames, and Streams.

Action Cable and WebSockets in Rails: Complete Guide for Technical Interviews 2026
Deep dive into Action Cable and WebSockets in Ruby on Rails. Covers connections, channels, broadcasting, Solid Cable in Rails 8, scaling with Redis, and common interview questions with code examples.