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 Interview Questions - Complete Guide

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.

Interview Tip

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.

ruby
# 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
ruby
# 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
erb
<%# 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.

ruby
# 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
ruby
# 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)
end

Active 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.

ruby
# 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
ruby
# 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
bash
# 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, seed

Migrations 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.

ruby
# 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
ruby
# 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
ruby
# 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 used

The 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.

ruby
# 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
ruby
# 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.

ruby
# 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
bash
# 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#drafts

Generated 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.

ruby
# 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
ruby
# 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
end

Callbacks 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.

ruby
# 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
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: '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
end

The 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.

ruby
# 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
ruby
# 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
ruby
# 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
end

Concerns 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.

ruby
# 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
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 '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
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 '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
end

Best 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.

ruby
# 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
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
# 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) }
end

Prefer 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.

ruby
# 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
ruby
# 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
ruby
# 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)
ruby
# 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: CleanupJob

Active 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.

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: '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
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
# 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
end

API 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.

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 has expired'
    rescue JWT::DecodeError
      raise AuthenticationError, 'Invalid token'
    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: '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
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, '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
end

For 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.

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 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 %>
ruby
# 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
ruby
# 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.

ruby
# 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
ruby
# 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
ruby
# 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
ruby
# 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
end

Essential 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.

ruby
# 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
ruby
# 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)
ruby
# 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] %>
ruby
# 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
ruby
# 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:'
end

Regularly 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.

ruby
# 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
ruby
# 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
ruby
# 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
end

Pundit 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.

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
# 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
end

Repository 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.

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, "Product #{item[:product_id]} not available")
        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 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
end

CQRS 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.

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
      # 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
ruby
# 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
end
app/javascript/channels/chat_channel.jsjavascript
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 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.

ruby
# 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
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
    # 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
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

  # Admins can belong to multiple tenants
  has_many :tenant_memberships
  has_many :accessible_tenants, through: :tenant_memberships, source: :tenant
end
ruby
# 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
end

Row-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.

ruby
# 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
ruby
# 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)
ruby
# 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
end

For 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.

ruby
# 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
# 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 --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:

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.

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 "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 } %>
ruby
# 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()
  }
}
ruby
# 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"
ruby
# 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...
ruby
# 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
end

Rails 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

#ruby on rails
#ruby
#interview
#active record
#technical interview

Share

Related articles