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
- Time zones: Use
Time.current, notTime.now - Callbacks cascading:
dependent: :destroycan delete entire trees - Memory leaks: Large
find_eachblocks holding references - Race conditions: Use pessimistic locking for critical sections
- Silent failures:
updatevsupdate!(bang methods raise errors) - Migration rollbacks: Always test
downmethod - Credential leaks: Never commit
master.keyor.envfiles
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.