Ruby on Rails Interview Questions (2025)

Rails Interview Questions Overview

Ruby on Rails powers Shopify, GitHub (partially), Airbnb, and many startups. Senior Rails interviews test deep ActiveRecord knowledge, query optimization, caching strategies, background job architecture, security, and Rails internals. This guide covers the questions that come up most frequently.

ActiveRecord and Database

What is the N+1 query problem and how do you fix it in Rails?

The N+1 problem occurs when you load a collection and then access an association for each record, generating N additional queries. Rails’s eager loading via includes, preload, or eager_load solves this:


# N+1 — generates 1 + N queries:
posts = Post.all
posts.each { |p| puts p.author.name }  # each .author fires a query

# Fix with includes (preloads in separate query):
posts = Post.includes(:author).all
posts.each { |p| puts p.author.name }  # 2 queries total

# eager_load (LEFT OUTER JOIN — useful for filtering on association):
posts = Post.eager_load(:author).where(authors: { active: true })

Use the Bullet gem in development to detect N+1 queries automatically. Use includes for most cases; eager_load when you need to filter on the association; preload when you explicitly want separate queries (avoids cartesian product for has_many).

What is the difference between find, find_by, and where?


User.find(1)          # SELECT WHERE id=1; raises RecordNotFound if missing
User.find_by(email: e) # SELECT WHERE email=? LIMIT 1; returns nil if missing
User.where(role: :admin) # returns ActiveRecord::Relation (lazy, chainable)
User.find_by!(email: e)  # like find_by but raises RecordNotFound

How do database indexes work in ActiveRecord?

Add indexes for columns used in WHERE, ORDER, and JOIN clauses. Missing indexes on foreign keys cause full table scans:


# Migration:
add_index :orders, :user_id                    # single column
add_index :orders, [:user_id, :status]         # composite — order matters
add_index :users, :email, unique: true         # unique constraint
add_index :products, :name, using: :gin,       # full-text search (PostgreSQL)
          opclass: { name: :gin_trgm_ops }

# Check query plan:
User.where(email: test@example.com).explain
# Look for "Index Scan" vs "Seq Scan"

Composite index (user_id, status) satisfies queries on user_id alone or on both (user_id, status) — but not status alone (leftmost prefix rule). Order matters.

Rails Caching

What caching strategies does Rails provide?

Rails has three levels of caching:

  • Fragment caching: cache rendered HTML partials with cache(key) { render partial }. The key auto-expires when the record’s updated_at changes (cache key includes the record). Works well for view fragments that are expensive to render.
  • Low-level caching: Rails.cache.fetch("key", expires_in: 1.hour) { expensive_computation }. Use Redis as the cache store in production (config.cache_store = :redis_cache_store).
  • HTTP caching: fresh_when(etag: resource, last_modified: resource.updated_at) — returns 304 Not Modified when the client has the current version. Eliminates response body transfer.

What is Russian Doll Caching?

Nested cache blocks where outer caches contain inner caches. A post partial caches comments; the outer page caches the post. When a comment changes, only the comment’s cache key changes (via updated_at); Rails automatically invalidates the post cache because the post’s cache key includes max(updated_at) across its comments (using touch: true on the association). Outer caches expire automatically without explicit invalidation logic.

Background Jobs

How do you design background job processing in Rails?

Use ActiveJob with Sidekiq (Redis-backed, highly performant). Key patterns:


class OrderConfirmationJob < ApplicationJob
  queue_as :critical  # separate queue for important jobs

  # Idempotency: safe to retry
  def perform(order_id)
    order = Order.find_by(order_id)
    return unless order  # order may have been deleted — safe exit
    return if order.confirmation_sent?  # idempotency check

    OrderMailer.confirmation(order).deliver_now
    order.update!(confirmation_sent_at: Time.current)
  end
end

# Enqueue:
OrderConfirmationJob.perform_later(order.id)
# Pass IDs, not ActiveRecord objects — objects can't be serialized safely

Always pass primitive IDs to jobs, not model objects (they may be stale when the job runs). Design jobs to be idempotent — network failures cause retries; the job must be safe to run twice. Use Sidekiq’s retry with exponential backoff; configure dead job queues for failed jobs needing manual intervention.

Security

What Rails security features should you know?

  • Strong parameters: params.require(:user).permit(:name, :email) — whitelist allowed attributes to prevent mass assignment attacks. Never pass params directly to update or create.
  • CSRF protection: Rails generates an authenticity token for every form. POST/PUT/DELETE requests without a valid token are rejected. Use protect_from_forgery (default in ApplicationController). API controllers that use token auth can skip it: skip_before_action :verify_authenticity_token.
  • SQL injection prevention: always use parameterized queries via ActiveRecord. Never interpolate user input into SQL: User.where("name = ?", params[:name]) is safe; User.where("name = #{params[:name]}") is SQL injection.
  • XSS prevention: ERB auto-escapes output ( is safe). Use html_safe only for trusted content; never for user input.
  • Content Security Policy: configure in config/initializers/content_security_policy.rb to restrict script sources.

Rails Architecture Patterns

When do you use service objects?

When business logic is complex, involves multiple models, or needs to be reusable across controllers and background jobs. A service object is a Plain Old Ruby Object (PORO) with a single responsibility:


class PlaceOrderService
  def initialize(user:, cart:, payment_method:)
    @user = user
    @cart = cart
    @payment_method = payment_method
  end

  def call
    ActiveRecord::Base.transaction do
      order = create_order
      reserve_inventory(order)
      charge_payment(order)
      send_confirmation(order)
      order
    end
  rescue PaymentError => e
    # rollback transaction, return error
    Result.failure(e.message)
  end

  private
  # ...
end

# In controller:
result = PlaceOrderService.new(user: current_user, cart: @cart,
                               payment_method: @pm).call

What is the difference between concerns and modules?

A Rails Concern is a module that extends ActiveSupport::Concern. It adds syntax sugar: included do ... end block runs in the context of the including class (for class-level declarations like has_many, validates, scope). Without Concern, you would need a base.class_eval block to achieve the same. Concerns encapsulate reusable model/controller behaviors (taggable, searchable, auditable) and are included in models: include Taggable.

Performance at Scale

  • counter_cache: add posts_count column to users table; belongs_to :user, counter_cache: true — Rails maintains the count automatically. Avoids COUNT(*) queries on every page load.
  • Pagination: use kaminari or pagy — never load full datasets. User.page(params[:page]).per(25).
  • select specific columns: User.select(:id, :name, :email) — avoids loading large columns (text, JSON blobs) you do not need.
  • Bulk operations: User.insert_all(array_of_hashes) and update_all skip callbacks for performance — use deliberately.
  • Connection pooling: set pool in database.yml to match Puma worker concurrency. Insufficient pool causes timeout errors under load.

Key Interview Takeaways

  • Fix N+1 with includes/eager_load; use Bullet gem to detect them in development
  • Index foreign keys and query columns; check EXPLAIN output for Seq Scans
  • Use Redis cache store; fragment cache with record-based cache keys for auto-expiration
  • Background jobs must be idempotent and receive IDs, not AR objects
  • Strong parameters prevent mass assignment; parameterized queries prevent SQL injection
  • Extract complex business logic into service objects for testability and reuse
Scroll to Top