LearnThatStack Ace your next interview

Ruby on Rails.
Cheatsheet.

Quick reference for Ruby on Rails - sectioned for fast scanning. Skim the part you're shaky on, walk in confident.

Backend Development 22-section reference ~12 min read

Overview

Ruby on Rails Cheat Sheet

Introduction

Ruby on Rails is a full-stack MVC framework that revolutionized web development with "convention over configuration" and "don't repeat yourself" principles. This cheatsheet covers Rails 7.0+ concepts essential for technical interviews, focusing on practical patterns, performance optimization, and modern Rails features including Hotwire. Key interview areas include ActiveRecord mastery, RESTful design, background job processing, and scaling strategies.

1. Rails Fundamentals

Core Principles

  • MVC Architecture: Separation of concerns (Model-View-Controller)
  • Convention over Configuration: Reduces decision fatigue with sensible defaults
  • DRY (Don't Repeat Yourself): Code reusability and maintainability
  • RESTful by Design: HTTP verbs map to CRUD operations
  • Active Record Pattern: Objects directly correspond to database rows

Rails 7 Directory Structure

app/
├── controllers/   # Request handling & response
├── models/        # Business logic & data persistence
├── views/         # HTML/JSON templates
├── javascript/    # Stimulus controllers & Turbo
├── jobs/          # Async background processing
├── mailers/       # Email composition & delivery
├── channels/      # Action Cable WebSockets
└── assets/        # CSS, images, fonts
config/
├── routes.rb      # URL to controller mapping
├── database.yml   # DB connection settings
└── credentials/   # Encrypted secrets

2. MVC Pattern & Request Cycle

Request Flow: Browser → Routes → Controller → Model → View → Response

# Model: Data logic & persistence
class User < ApplicationRecord
  validates :email, presence: true, uniqueness: true
  has_secure_password # Rails 7 built-in bcrypt
end

# Controller: Request orchestration
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update]
  
  def show
    # @user available to view
  end
  
  private
  def set_user
    @user = User.find(params[:id])
  end
end

# View: Presentation (ERB/Turbo)
<turbo-frame id="user_<%= @user.id %>">
  <%= @user.name %>
</turbo-frame>

3. Routing & RESTful Design

Advanced Routing Patterns

# config/routes.rb
Rails.application.routes.draw do
  # API versioning
  namespace :api do
    namespace :v1 do
      resources :users, only: [:index, :show]
    end
  end
  
  # Resources with constraints
  resources :posts do
    resources :comments, shallow: true # Reduces nesting depth
    member do
      post 'publish'    # POST /posts/:id/publish
    end
    collection do
      get 'trending'    # GET /posts/trending
    end
  end
  
  # Route constraints
  get 'users/:id', to: 'users#show', constraints: { id: /\d+/ }
  
  # Direct routes & redirects
  direct(:github) { "https://github.com/rails" }
  get '/legacy', to: redirect('/posts')
  
  root 'dashboard#index'
end

Helper Methods: posts_url (full URL) vs posts_path (relative path)

4. ActiveRecord Mastery

Query Interface & Performance

# Efficient querying - O(1) for find, O(n) for where
User.find(1)                          # SELECT * FROM users WHERE id = 1 LIMIT 1
User.find_by!(email: 'a@b.com')       # Raises error if not found
User.where(active: true).or(User.where(admin: true))  # OR conditions

# Modern Rails 7 methods
User.sole                             # Expects exactly 1 record, errors otherwise
User.insert_all([{name: 'A'}, {name: 'B'}])  # Bulk insert, skips validations
User.upsert_all([{id: 1, name: 'Updated'}])  # Insert or update

# Query optimization
User.select(:id, :name)               # SELECT only needed columns
User.pluck(:email)                    # Returns array, bypasses instantiation
User.pick(:id, :name)                 # Like pluck but returns single record

# Batching for large datasets
User.find_each(batch_size: 1000) { |u| u.process }  # Memory efficient
User.in_batches.update_all(status: 'processed')     # Bulk updates

Advanced Scopes & Class Methods

class Post < ApplicationRecord
  scope :published, -> { where(published: true) }
  scope :by_author, ->(name) { joins(:author).where(authors: { name: name }) }
  
  # Prefer class methods for complex logic
  def self.trending(days = 7)
    where('created_at > ?', days.days.ago)
      .group(:category_id)
      .having('COUNT(*) > ?', 10)
  end
end

5. Models, Validations & Callbacks

Validations with Context

class User < ApplicationRecord
  # Conditional validations
  validates :email, presence: true, uniqueness: { case_sensitive: false }
  validates :terms, acceptance: true, on: :create
  validates :password, confirmation: true, if: :password_required?
  validates :age, numericality: { in: 18..100 }, allow_nil: true
  
  # Custom validators
  validate :email_domain_check, on: :update
  validates_with EmailValidator # Custom validator class
  
  private
  def email_domain_check
    return if email.blank?
    errors.add(:email, "must be company email") unless email.match?(/@company\.(com|org)$/)
  end
end

Callback Lifecycle (Correct Order)

class Article < ApplicationRecord
  # Create: validation → save → create → commit
  before_validation :strip_whitespace
  after_validation :set_slug
  before_save :encrypt_content
  before_create :set_published_at     # Only on create
  after_create :notify_subscribers     # Only on create
  after_save :clear_cache              # On create & update
  after_commit :send_notifications     # After DB commit
  after_rollback :log_failure          # On transaction rollback
  
  # ⚠️ Interview Red Flag: Using after_save for external API calls
  # Use after_commit to ensure transaction success
end

6. Associations & Eager Loading

Association Types with Performance Considerations

# belongs_to (foreign key on this table)
class Post < ApplicationRecord
  belongs_to :user, counter_cache: true  # Maintains users.posts_count
  belongs_to :category, optional: true, touch: true  # Updates category.updated_at
end

# has_many with advanced options
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :published_posts, -> { where(published: true) }, class_name: 'Post'
  has_many :recent_posts, -> { order(created_at: :desc).limit(5) }, class_name: 'Post'
  
  # Through association (join model)
  has_many :post_tags, through: :posts, source: :tags
end

# Many-to-Many with rich join model
class Enrollment < ApplicationRecord
  belongs_to :student, class_name: 'User'
  belongs_to :course
  # Join model can have its own attributes
  validates :grade, inclusion: { in: %w[A B C D F] }
end

# Polymorphic for flexible relationships
class Image < ApplicationRecord
  belongs_to :imageable, polymorphic: true
  # Requires imageable_type and imageable_id columns
end

N+1 Query Prevention

# ❌ N+1 Problem (1 + N queries)
users = User.all
users.each { |u| puts u.posts.count }

# ✅ Solutions:
User.includes(:posts)        # LEFT OUTER JOIN, loads all data
User.preload(:posts)         # Separate queries, no JOIN
User.eager_load(:posts)      # LEFT OUTER JOIN, allows WHERE on association
User.joins(:posts)           # INNER JOIN, doesn't load association data

# Complex eager loading
Post.includes(user: :profile, comments: :author)

7. Migrations & Schema Management

Rails 7 Migration Best Practices

class CreateProducts < ActiveRecord::Migration[7.0]
  def change
    create_table :products do |t|
      t.string :name, null: false
      t.text :description
      t.decimal :price, precision: 10, scale: 2, default: 0
      t.boolean :active, default: true, index: true
      t.references :category, null: false, foreign_key: true
      t.jsonb :metadata, default: {}  # PostgreSQL JSON
      t.virtual :full_name, type: :string, as: "name || ' ' || sku"  # Generated column
      t.timestamps
    end
    
    add_index :products, :name
    add_index :products, [:category_id, :active]  # Composite index
    add_check_constraint :products, "price >= 0", name: "price_positive"
  end
end

# Reversible migrations
class AddDetailsToProducts < ActiveRecord::Migration[7.0]
  def up
    add_column :products, :sku, :string
    Product.reset_column_information
    Product.update_all(sku: 'DEFAULT-SKU')
    change_column_null :products, :sku, false
  end
  
  def down
    remove_column :products, :sku
  end
end

8. Controllers & Request Handling

Modern Controller Patterns

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!, except: [:index, :show]
  
  # GET /posts - with pagination & filtering
  def index
    @posts = Post.published
                 .includes(:author, :tags)  # Prevent N+1
                 .page(params[:page])
                 .per(20)
  end
  
  # POST /posts - with Turbo response
  def create
    @post = current_user.posts.build(post_params)
    
    if @post.save
      respond_to do |format|
        format.html { redirect_to @post, notice: 'Created successfully' }
        format.turbo_stream { render turbo_stream: turbo_stream.prepend('posts', @post) }
        format.json { render json: @post, status: :created }
      end
    else
      render :new, status: :unprocessable_entity
    end
  end
  
  private
  
  def set_post
    @post = Post.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    redirect_to posts_path, alert: 'Post not found'
  end
  
  def post_params
    params.require(:post).permit(:title, :body, tag_ids: [], 
                                  metadata: {})  # Permit nested/array params
  end
end

Action Controller Concerns

module Authenticable
  extend ActiveSupport::Concern
  
  included do
    before_action :authenticate_user!
    helper_method :current_user
  end
  
  private
  
  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end
end

9. Background Jobs & ActionMailer

ActiveJob with Sidekiq/Redis

class ProcessPaymentJob < ApplicationJob
  queue_as :critical
  retry_on Net::OpenTimeout, wait: 5.seconds, attempts: 3
  discard_on ActiveJob::DeserializationError
  
  def perform(payment_id)  # Pass IDs, not objects
    payment = Payment.find(payment_id)
    PaymentProcessor.new(payment).charge!
  rescue StandardError => e
    ErrorTracker.report(e, payment_id: payment_id)
    raise  # Re-raise to trigger retry
  end
end

# Enqueue strategies
ProcessPaymentJob.perform_later(payment.id)
ProcessPaymentJob.set(wait: 5.minutes).perform_later(payment.id)
ProcessPaymentJob.set(wait_until: Date.tomorrow.noon).perform_later(payment.id)

ActionMailer Best Practices

class UserMailer < ApplicationMailer
  default from: 'notifications@example.com'
  layout 'mailer'  # Uses app/views/layouts/mailer.html.erb
  
  def welcome_email(user_id)  # Pass ID, not object
    @user = User.find(user_id)
    attachments['terms.pdf'] = File.read('path/to/terms.pdf')
    
    mail(
      to: email_address_with_name(@user.email, @user.name),
      subject: 'Welcome to Our App!',
      track_opens: true  # If using SendGrid/Mailgun
    )
  end
end

# Always use deliver_later for better performance
UserMailer.welcome_email(@user.id).deliver_later

10. Security & Authentication

Modern Security Patterns

# SQL Injection Prevention
User.where(name: params[:name])  # ✅ Parameterized
User.where('name LIKE ?', "%#{params[:search]}%")  # ✅ Safe LIKE
User.where("role IN (?)", params[:roles])  # ✅ Safe IN

# Authentication with has_secure_password
class User < ApplicationRecord
  has_secure_password
  has_secure_token :auth_token  # For API authentication
  
  # Password complexity validation
  validates :password, format: {
    with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}\z/,
    message: 'must include uppercase, lowercase, and number'
  }, on: :create
end

# Session management
class SessionsController < ApplicationController
  def create
    user = User.find_by(email: params[:email])
    if user&.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to root_path
    else
      flash.now[:alert] = 'Invalid credentials'
      render :new, status: :unprocessable_entity
    end
  end
end

# CSRF & Security headers
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  
  # Content Security Policy
  content_security_policy do |policy|
    policy.default_src :self
    policy.script_src :self, 'https://cdn.jsdelivr.net'
  end
end

11. Testing with RSpec

Model & Integration Tests

# spec/models/user_spec.rb
RSpec.describe User, type: :model do
  # Use factories, not fixtures
  let(:user) { create(:user) }
  
  describe 'validations' do
    it { should validate_presence_of(:email) }
    it { should validate_uniqueness_of(:email).case_insensitive }
  end
  
  describe 'associations' do
    it { should have_many(:posts).dependent(:destroy) }
    it { should have_one(:profile) }
  end
  
  describe '#full_name' do
    it 'concatenates first and last name' do
      user = build(:user, first_name: 'John', last_name: 'Doe')
      expect(user.full_name).to eq('John Doe')
    end
  end
end

# spec/requests/posts_spec.rb (Preferred over controller tests)
RSpec.describe 'Posts API', type: :request do
  let(:user) { create(:user) }
  
  before { sign_in user }  # Helper method
  
  describe 'GET /posts' do
    it 'returns paginated posts' do
      create_list(:post, 25)
      get posts_path
      
      expect(response).to have_http_status(:ok)
      expect(JSON.parse(response.body)['posts'].size).to eq(20)
    end
  end
end

12. Caching Strategies

Multi-Layer Caching

# Russian Doll Caching (nested fragments)
# app/views/posts/show.html.erb
<% cache @post do %>
  <h1><%= @post.title %></h1>
  <% cache [@post, 'comments'] do %>
    <%= render @post.comments %>
  <% end %>
<% end %>

# Model caching with cache key versioning
class Post < ApplicationRecord
  # Automatically updates cache when model changes
  def cache_key_with_version
    "#{model_name.cache_key}/#{id}/#{updated_at.to_i}"
  end
  
  # Method caching
  def expensive_calculation
    Rails.cache.fetch([cache_key_with_version, 'calculation'], expires_in: 1.hour) do
      # Complex computation
    end
  end
end

# Redis caching for sessions/temporary data
Rails.cache.write('user:123:cart', items, expires_in: 30.minutes)
Rails.cache.increment('page_views')  # Atomic counter

13. API Development

RESTful API with JWT Authentication

# app/controllers/api/v1/base_controller.rb
class Api::V1::BaseController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods
  
  before_action :authenticate
  
  private
  
  def authenticate
    authenticate_or_request_with_http_token do |token, options|
      @current_user = User.find_by(auth_token: token)
    end
  end
end

# app/controllers/api/v1/posts_controller.rb
class Api::V1::PostsController < Api::V1::BaseController
  def index
    posts = Post.includes(:author)
                .page(params[:page])
                .per(params[:per_page] || 25)
    
    render json: {
      posts: ActiveModelSerializers::SerializableResource.new(posts),
      meta: pagination_meta(posts)
    }
  end
  
  def create
    post = @current_user.posts.build(post_params)
    
    if post.save
      render json: post, status: :created
    else
      render json: { errors: post.errors.full_messages }, 
             status: :unprocessable_entity
    end
  end
  
  private
  
  def pagination_meta(collection)
    {
      current_page: collection.current_page,
      total_pages: collection.total_pages,
      total_count: collection.total_count
    }
  end
end

14. Modern Rails Features

Hotwire (Turbo + Stimulus)

# Turbo Frames - Partial page updates
# app/views/posts/edit.html.erb
<%= turbo_frame_tag @post do %>
  <%= form_with model: @post do |f| %>
    <%= f.text_field :title %>
    <%= f.submit %>
  <% end %>
<% end %>

# Turbo Streams - Live updates
class CommentsController < ApplicationController
  def create
    @comment = @post.comments.create!(comment_params)
    
    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: [
          turbo_stream.append('comments', @comment),
          turbo_stream.update('comment_count', @post.comments.count)
        ]
      end
      format.html { redirect_to @post }
    end
  end
end

# Stimulus Controller
// app/javascript/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = [ "output" ]
  
  connect() {
    this.outputTarget.textContent = "Hello, Stimulus!"
  }
}

Action Cable (WebSockets)

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room_id]}"
  end
  
  def speak(data)
    ActionCable.server.broadcast(
      "chat_#{params[:room_id]}",
      message: data['message'],
      user: current_user.name
    )
  end
end

# Broadcasting from anywhere
ChatChannel.broadcast_to(
  'room_123',
  { message: 'Hello', user: 'System' }
)

Active Storage

class User < ApplicationRecord
  has_one_attached :avatar
  has_many_attached :documents
  
  # Validations
  validates :avatar, content_type: ['image/png', 'image/jpg'],
                     size: { less_than: 5.megabytes }
end

# Controller
def update
  @user.avatar.attach(params[:avatar])
  # Variants for images
  @user.avatar.variant(resize_to_limit: [100, 100])
end

# Direct uploads from frontend
<%= form.file_field :avatar, direct_upload: true %>

15. Performance Optimization

Database & Query Optimization

# Explain query plan
User.where(active: true).explain

# Bullet gem for N+1 detection in development
# Gemfile
gem 'bullet', group: :development

# Query optimization techniques
User.pluck(:id, :email)              # [id, email] arrays, no AR objects
User.pick(:email)                     # Single value, first record
User.select(:id, :name).distinct      # Only needed columns
User.where(id: ids).in_order_of(:id, ids)  # Maintain order

# Database connection pooling
# config/database.yml
production:
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 25 } %>
  checkout_timeout: 5
  reaping_frequency: 10

# Query result caching within request
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])  # Query executed
    @same_post = Post.find(params[:id])  # Cached, no query
  end
end

# Database views for complex queries
class PopularPostsView < ApplicationRecord
  self.table_name = 'popular_posts_view'
  # CREATE VIEW popular_posts_view AS SELECT ...
end

Application Performance

# Lazy loading with load_async (Rails 7)
posts = Post.load_async  # Non-blocking query
users = User.load_async
# Queries execute in parallel

# Memory optimization
Post.find_each(batch_size: 500) do |post|
  post.process  # Releases memory after each batch
end

# Request-level caching
def current_user
  @current_user ||= User.find(session[:user_id])
end

16. Design Patterns & Best Practices

Service Objects (Business Logic)

class PaymentProcessor
  def self.call(...)
    new(...).call
  end
  
  def initialize(order, payment_method)
    @order = order
    @payment_method = payment_method
  end
  
  def call
    return failure('Invalid order') unless valid?
    
    ActiveRecord::Base.transaction do
      charge_payment
      update_order_status
      send_confirmation
    end
    
    success(@order)
  rescue => e
    failure(e.message)
  end
  
  private
  
  def success(data)
    OpenStruct.new(success?: true, data: data)
  end
  
  def failure(error)
    OpenStruct.new(success?: false, error: error)
  end
end

Form Objects (Complex Forms)

class RegistrationForm
  include ActiveModel::Model
  
  attr_accessor :email, :password, :company_name
  
  validates :email, presence: true, format: URI::MailTo::EMAIL_REGEXP
  validates :password, length: { minimum: 8 }
  
  def save
    return false unless valid?
    
    ActiveRecord::Base.transaction do
      user = User.create!(email: email, password: password)
      Company.create!(name: company_name, owner: user)
    end
    
    true
  rescue
    errors.add(:base, 'Registration failed')
    false
  end
end

Query Objects (Complex Queries)

class PostsQuery
  def initialize(relation = Post.all)
    @relation = relation
  end
  
  def published
    @relation = @relation.where(published: true)
    self
  end
  
  def by_author(author)
    @relation = @relation.where(author: author)
    self
  end
  
  def recent(days = 7)
    @relation = @relation.where('created_at > ?', days.days.ago)
    self
  end
  
  def resolve
    @relation
  end
end

# Usage: PostsQuery.new.published.recent.resolve

17. Rails CLI Commands

# Rails 7 commands (not rake)
rails new app --database=postgresql --css=tailwind
rails generate model User email:uniq password:digest
rails generate controller Api::V1::Users --api
rails generate stimulus hello

# Database
rails db:create db:migrate db:seed  # Chain commands
rails db:migrate:status              # Check migration status
rails db:rollback STEP=3             # Rollback 3 migrations
rails db:schema:load                 # Load from schema.rb

# Console tricks
rails c --sandbox  # Rollback changes on exit
reload!           # Reload console environment
app.users_path    # Test routes
helper.time_ago_in_words(1.day.ago)

# Asset management
rails assets:precompile
rails assets:clean

# Credentials
rails credentials:edit --environment=production
Rails.application.credentials.aws[:access_key_id]

18. Interview Red Flags & Common Mistakes

⚠️ Code Smells to Avoid

# ❌ Fat controllers
class UsersController < ApplicationController
  def create
    # 50+ lines of business logic
  end
end

# ✅ Use service objects for complex logic

# ❌ N+1 queries in views
<% @posts.each do |post| %>
  <%= post.user.name %>  # N+1!
<% end %>

# ✅ Eager load in controller
@posts = Post.includes(:user)

# ❌ Business logic in views
<% if @user.orders.sum(&:total) > 1000 %>

# ✅ Move to model or decorator
def vip_customer?
  orders.sum(:total) > 1000
end

# ❌ Synchronous external API calls
class OrdersController < ApplicationController
  def create
    PaymentGateway.charge(...)  # Blocks request
  end
end

# ✅ Use background jobs
PaymentJob.perform_later(...)

Critical Gotchas

  1. Time zones: Use Time.current, not Time.now
  2. Callbacks cascading: dependent: :destroy can delete entire trees
  3. Memory leaks: Large find_each blocks holding references
  4. Race conditions: Use pessimistic locking for critical sections
  5. Silent failures: update vs update! (bang methods raise errors)
  6. Migration rollbacks: Always test down method
  7. Credential leaks: Never commit master.key or .env files

19. Scaling & Production Considerations

Database Scaling

# Read/Write splitting
class ApplicationRecord < ActiveRecord::Base
  connects_to database: {
    writing: :primary,
    reading: :replica
  }
end

# Sharding
class User < ApplicationRecord
  connects_to shards: {
    shard_one: { writing: :primary_shard_one },
    shard_two: { writing: :primary_shard_two }
  }
end

Performance Monitoring

# APM Integration (NewRelic, DataDog, Scout)
class ApplicationController < ActionController::Base
  around_action :track_performance
  
  def track_performance
    start = Time.current
    yield
  ensure
    duration = Time.current - start
    Rails.logger.info "Action took #{duration}s"
    StatsD.timing("controller.#{controller_name}.#{action_name}", duration)
  end
end

Load Balancing & Deployment

  • Puma configuration for concurrency
  • Redis for caching & sessions
  • CDN for assets (CloudFront, Fastly)
  • Docker containerization
  • Kubernetes for orchestration
  • Blue-Green deployments for zero downtime

20. Quick Review Checklist

Must-Know Concepts

✓ MVC architecture & request cycle
✓ RESTful routing & resourceful controllers
✓ ActiveRecord queries & N+1 prevention
✓ Associations (belongs_to, has_many, has_many :through)
✓ Validations & callbacks lifecycle
✓ Strong parameters & security
✓ Background jobs with ActiveJob
✓ Testing with RSpec/Minitest
✓ Caching strategies (fragment, Russian doll)
✓ Database migrations & schema management

Advanced Topics

✓ Service objects & design patterns
✓ API development & authentication
✓ WebSockets with Action Cable
✓ File uploads with Active Storage
✓ Hotwire (Turbo + Stimulus)
✓ Performance optimization
✓ Database scaling strategies


Interview Tip: Focus on understanding concepts over memorizing syntax. Be ready to discuss trade-offs, performance implications, and real-world problem-solving approaches.

Found this useful? Pass it on.
Pro · $10/mo

The sheet is free. Pro goes deeper.

Pro opens the full question library behind every sheet, every refresher and a monthly AI allowance. One subscription, all formats.

Full question library All refreshers Cancel anytime