All topics
Backend · Learning hub

Ruby notes for developers

Master Ruby with a curated set of 7 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Backend notes
Ruby

Language Fundamentals

Ruby: Language Fundamentals Ruby is a dynamic, object-oriented language designed for developer happiness. Everything is an object. Known for its elegant syntax

Ruby: Language Fundamentals

Ruby is a dynamic, object-oriented language designed for developer happiness. Everything is an object. Known for its elegant syntax and the Rails framework.

Data Types & Variables

# Variables
local_var = 42          # local
@instance_var = "hello" # instance (within class)
@@class_var = []        # class (shared by all instances)
$global_var = "global"  # global (avoid)
CONSTANT = 3.14         # constant (convention: ALL_CAPS)

# Symbols (immutable identifiers — like frozen strings)
:name    # cheaper than strings for hash keys, comparisons
:name == :name  # always the same object in memory

# String
name = "Alice"
greeting = "Hello, #{name}!"    # interpolation (double quotes only)
multiline = <<~HEREDOC
  Line one
  Line two
HEREDOC

# Ranges
(1..10)   # inclusive: 1 to 10
(1...10)  # exclusive: 1 to 9
('a'..'z').to_a  # ['a', 'b', ..., 'z']

# Array
arr = [1, "two", :three, [4, 5]]
arr.first    # 1
arr.last     # [4, 5]
arr[-1]      # [4, 5]  (negative indexing)
arr[1..2]    # ["two", :three]

# Hash
user = { name: "Alice", age: 30 }  # symbol keys (modern syntax)
user[:name]     # "Alice"
user[:missing]  # nil (no KeyError)
user.fetch(:missing, "default")  # "default"

Control Flow

# Conditionals
if score >= 90
  "A"
elsif score >= 80
  "B"
else
  "C"
end

# Unless (if not)
unless user.admin?
  redirect_to root_path
end

# Ternary
status = active ? "on" : "off"

# Case (powerful — matches with ===)
case value
when 1..10       then "low"
when String      then "it's a string"
when /error/i    then "error pattern"
else             "other"
end

# Loops
5.times { |i| puts i }
(1..5).each { |i| puts i }

[1, 2, 3].each do |n|
  puts n
end

while condition
  # ...
  break if done
  next if skip
end

Methods & Blocks

# Method definition
def greet(name, greeting: "Hello")  # keyword argument with default
  "#{greeting}, #{name}!"
end

greet("Alice")                  # "Hello, Alice!"
greet("Bob", greeting: "Hi")   # "Hi, Bob!"

# Variadic arguments
def sum(*numbers)
  numbers.reduce(0, :+)
end

# Block — anonymous function passed to a method
[1, 2, 3].each { |n| puts n }
[1, 2, 3].each do |n|
  puts n
end

# yield — call the block passed to a method
def repeat(n)
  n.times { yield }
end
repeat(3) { puts "Hello" }

# Explicit block parameter
def log(&block)
  result = block.call
  puts "Result: #{result}"
end

# Proc and Lambda
double = Proc.new { |x| x * 2 }
triple = lambda { |x| x * 3 }   # or: ->(x) { x * 3 }
double.call(5)   # 10
triple.(5)       # 15

Enumerables

nums = [1, 2, 3, 4, 5, 6]

nums.map { |n| n * 2 }           # [2, 4, 6, 8, 10, 12]
nums.select { |n| n.even? }      # [2, 4, 6]
nums.reject { |n| n.even? }      # [1, 3, 5]
nums.find { |n| n > 3 }          # 4
nums.reduce(0) { |sum, n| sum + n }  # 21 (also: nums.sum)
nums.min    # 1
nums.max    # 6
nums.sort   # [1, 2, 3, 4, 5, 6]
nums.sort_by { |n| -n }          # [6, 5, 4, 3, 2, 1]
nums.group_by(&:even?)           # {false=>[1,3,5], true=>[2,4,6]}
nums.any? { |n| n > 5 }         # true
nums.all? { |n| n > 0 }         # true
nums.none? { |n| n > 10 }       # true
nums.count { |n| n.odd? }       # 3
nums.flat_map { |n| [n, n * 2] }  # [1,2,2,4,3,6,...]
nums.each_with_object({}) { |n, h| h[n] = n**2 }  # {1=>1, 2=>4, ...}
Ruby

OOP, Modules & Mixins

Ruby: OOP, Modules & Mixins Classes class Animal attr_accessor :name, :age # generates getter + setter attr_reader :species # getter only attr_writer :nickname

Ruby: OOP, Modules & Mixins

Classes

class Animal
  attr_accessor :name, :age  # generates getter + setter
  attr_reader :species       # getter only
  attr_writer :nickname      # setter only

  @@count = 0               # class variable

  def initialize(name, age, species)
    @name = name
    @age = age
    @species = species
    @@count += 1
  end

  def self.count             # class method
    @@count
  end

  def to_s                   # string representation
    "#{@species} named #{@name}"
  end

  def <=>(other)             # comparison (enables sort)
    @age <=> other.age
  end

  protected

  def secret_method          # only accessible within class hierarchy
  end

  private

  def internal               # only accessible within this object
  end
end

class Dog < Animal           # inheritance
  def initialize(name, age)
    super(name, age, "Dog")  # call parent initialize
  end

  def speak
    "Woof!"
  end

  def to_s
    super + " (dog)"         # call parent to_s
  end
end

rex = Dog.new("Rex", 3)
puts rex         # "Dog named Rex (dog)"
Dog.count        # 1

Modules & Mixins

# Module: namespace + mixin (can't be instantiated)
module Greetable
  def greet
    "Hello, I am #{name}"
  end

  def farewell
    "Goodbye from #{name}"
  end
end

module Serializable
  def to_json
    instance_variables.each_with_object({}) do |var, h|
      h[var.to_s.delete('@')] = instance_variable_get(var)
    end.to_json
  end
end

class Person
  include Greetable     # mixin — adds instance methods
  include Serializable
  extend Greetable      # extend — adds as class methods

  attr_reader :name

  def initialize(name)
    @name = name
  end
end

alice = Person.new("Alice")
alice.greet      # "Hello, I am Alice"
alice.to_json    # '{"name":"Alice"}'

# Comparable mixin — adds <, <=, >, >=, between?, clamp
class Box
  include Comparable
  attr_reader :volume

  def initialize(volume)
    @volume = volume
  end

  def <=>(other)
    volume <=> other.volume
  end
end

# Enumerable mixin — adds map, select, sort, etc.
class WordCollection
  include Enumerable

  def initialize(words)
    @words = words
  end

  def each(&block)           # Enumerable requires each
    @words.each(&block)
  end
end

Duck Typing & Interfaces

  • Ruby has no interfaces — uses duck typing: "if it responds to the method, it works"

  • respond_to?(:method_name) — check before calling a method on an unknown object

  • is_a? / kind_of? — check class membership

  • Comparable, Enumerable — standard mixins that define protocols through duck typing

  • Struct: Struct.new(:name, :age) — creates a simple class with accessors and equality

Point = Struct.new(:x, :y) do
  def distance_to(other)
    Math.sqrt((x - other.x)**2 + (y - other.y)**2)
  end
end

p = Point.new(0, 0)
q = Point.new(3, 4)
p.distance_to(q)  # 5.0
p == Point.new(0, 0)  # true (value equality)
Ruby

Standard Library, Gems & Tooling

Ruby: Standard Library, Gems & Tooling Standard Library Highlights require 'json' JSON.parse('{"name": "Alice"}') # => {"name" => "Alice"} { name: "Alice" }.to_

Ruby: Standard Library, Gems & Tooling

Standard Library Highlights

require 'json'
JSON.parse('{"name": "Alice"}')  # => {"name" => "Alice"}
{ name: "Alice" }.to_json        # => '{"name":"Alice"}'

require 'date'
Date.today                        # #<Date: 2026-05-07>
Date.parse("2026-01-15")
DateTime.now
Time.now.strftime("%Y-%m-%d %H:%M")

require 'net/http'
require 'uri'
uri = URI('https://api.example.com/users')
response = Net::HTTP.get_response(uri)
JSON.parse(response.body)

require 'fileutils'
FileUtils.mkdir_p('path/to/dir')
FileUtils.cp('src.txt', 'dst.txt')
FileUtils.rm_rf('temp/')

require 'erb'
template = ERB.new("Hello, <%= name %>!")
template.result(binding)   # "Hello, Alice!" (uses local binding)

require 'digest'
Digest::SHA256.hexdigest("password")
Digest::MD5.hexdigest("data")

Gems & Bundler

# Gemfile
source "https://rubygems.org"
ruby "3.3.0"

gem "rails", "~> 7.2"          # ~> means >= 7.2, < 8.0
gem "pg"                        # PostgreSQL adapter
gem "puma"                      # Web server
gem "sidekiq"                   # Background jobs

group :development, :test do
  gem "rspec-rails"
  gem "factory_bot_rails"
end

group :test do
  gem "faker"
  gem "shoulda-matchers"
end

# Bundler commands
bundle install          # install gems from Gemfile
bundle update rails     # update specific gem
bundle exec rspec       # run command in bundle context
bundle exec rake db:migrate

gem install bundler     # install bundler itself
gem list                # list installed gems

RSpec Testing

# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  let(:user) { build(:user) }             # FactoryBot

  describe 'validations' do
    it { is_expected.to validate_presence_of(:email) }
    it { is_expected.to validate_uniqueness_of(:email) }
  end

  describe '#full_name' do
    it 'combines first and last name' do
      user = described_class.new(first_name: 'Alice', last_name: 'Smith')
      expect(user.full_name).to eq('Alice Smith')
    end
  end

  context 'when admin' do
    let(:admin) { create(:user, :admin) }

    it 'can access admin panel' do
      expect(admin.can?(:access, :admin_panel)).to be true
    end
  end
end

Version Management & Tooling

# rbenv (recommended version manager)
rbenv install 3.3.2
rbenv global 3.3.2
rbenv local 3.2.0      # .ruby-version in project dir

# RVM (alternative)
rvm install 3.3.2
rvm use 3.3.2 --default

# Common tools
gem install solargraph    # LSP for IDE support
gem install rubocop       # linter/formatter
gem install brakeman      # security scanner
gem install pry           # better REPL than irb

# Interactive Ruby
irb                       # built-in REPL
pry                       # enhanced REPL with debugging

# Rake tasks
rake -T                   # list all tasks
rake db:migrate
rake test
Ruby

Metaprogramming & Interview Questions

Ruby: Metaprogramming & Interview Questions Metaprogramming Basics Ruby's metaprogramming capabilities allow code to write code at runtime. This powers Rails ma

Ruby: Metaprogramming & Interview Questions

Metaprogramming Basics

Ruby's metaprogramming capabilities allow code to write code at runtime. This powers Rails magic like has_many, validates, and attr_accessor.

# define_method — create methods dynamically
class User
  ['admin', 'moderator', 'guest'].each do |role|
    define_method("#{role}?") do
      self.role == role
    end
  end
end

user.admin?      # true/false

# method_missing — intercept unknown method calls
class DynamicProxy
  def initialize(target)
    @target = target
  end

  def method_missing(name, *args, &block)
    if @target.respond_to?(name)
      @target.send(name, *args, &block)
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @target.respond_to?(name) || super
  end
end

# open classes — reopen and extend any class
class String
  def palindrome?
    self == self.reverse
  end
end

"racecar".palindrome?   # true

# send — call method by name (even private)
user.send(:name)
user.public_send(:name)   # raises error if private

# eval, class_eval, instance_eval
String.class_eval do
  def shout
    upcase + "!!!"
  end
end

Symbol#to_proc

# Common shorthand using &
[1, 2, 3].map(&:to_s)         # ["1", "2", "3"]
["alice", "bob"].map(&:upcase) # ["ALICE", "BOB"]
[1, nil, 2, nil].compact       # remove nils
[1, nil, 2, nil].select(&:itself)  # same

# Equivalent to:
[1, 2, 3].map { |n| n.to_s }

# tap — for debugging in method chains
user.tap { |u| puts u.inspect }
    .update!(name: "Alice")
    .tap { |u| puts "Updated: #{u.name}" }

Frozen Objects & Performance

# String literals are mutable by default — freeze for safety and performance
str = "hello".freeze
str << " world"   # FrozenError

# frozen_string_literal magic comment (Ruby 2.3+)
# frozen_string_literal: true
# All string literals in this file are frozen

# Symbols are always frozen and deduplicated
:hello.frozen?   # true
:hello.object_id == :hello.object_id  # true

# Object#freeze
arr = [1, 2, 3].freeze
arr << 4   # FrozenError

Interview Questions

  • What is the difference between nil, false, and undefined in Ruby? nil and false are falsy; everything else is truthy. Undefined variables raise NameError.

  • Difference between Symbol and String? Symbols are immutable, interned (same object_id for same value), faster for comparisons and hash keys. Strings are mutable, each literal is a new object.

  • What does &method(:method_name) do? Converts a method reference to a block — useful for passing existing methods to enumerables: [1,2,3].map(&method(:puts))

  • Explain Ruby's object model: Everything inherits from BasicObject. Classes are objects (instances of Class). Modules are included in the method lookup chain (ancestors).

  • What is a Proc vs Lambda? Both are callable objects. Lambda checks argument count and returns from itself. Proc doesn't check arity and return exits the enclosing method.

  • What is Comparable? A mixin that requires <=> and provides <, <=, >, >=, between?, clamp for free.

  • How does Ruby handle multiple inheritance? Ruby doesn't have it — uses modules (mixins) included in order. The lookup chain (ancestors) determines which method is found first.

Ruby

Rails Fundamentals

Ruby: Rails Fundamentals Ruby on Rails is a full-stack web framework built on the MVC pattern. Convention over configuration: Rails generates predictable paths,

Ruby: Rails Fundamentals

Ruby on Rails is a full-stack web framework built on the MVC pattern. Convention over configuration: Rails generates predictable paths, names, and structures so you can focus on business logic.

Routes

# config/routes.rb
Rails.application.routes.draw do
  # RESTful resources — generates 7 standard routes (index, show, new, create, edit, update, destroy)
  resources :users
  resources :posts do
    resources :comments, only: [:create, :destroy]  # nested
    member do
      post :publish           # POST /posts/:id/publish
    end
    collection do
      get :archived           # GET /posts/archived
    end
  end

  # Custom routes
  get  '/login',  to: 'sessions#new'
  post '/login',  to: 'sessions#create'
  delete '/logout', to: 'sessions#destroy'

  # Namespace (admin panel)
  namespace :admin do
    resources :users          # /admin/users → Admin::UsersController
  end

  root 'home#index'           # GET / → HomeController#index
end

# Inspect routes
# rails routes
# rails routes --grep users

Controllers

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  before_action :authorize_owner!, only: [:edit, :update, :destroy]

  def index
    @posts = Post.published.order(created_at: :desc).page(params[:page])
    render json: @posts   # or just let it render index.html.erb
  end

  def show
    # @post set by before_action
  end

  def create
    @post = current_user.posts.build(post_params)
    if @post.save
      redirect_to @post, notice: 'Post created!'
    else
      render :new, status: :unprocessable_entity
    end
  end

  def update
    if @post.update(post_params)
      redirect_to @post
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @post.destroy
    redirect_to posts_path, status: :see_other
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end

  def post_params
    params.require(:post).permit(:title, :body, :published, tag_ids: [])
  end

  def authorize_owner!
    redirect_to root_path unless @post.user == current_user
  end
end

ActiveRecord: Models & Associations

# app/models/user.rb
class User < ApplicationRecord
  # Associations
  has_many :posts, dependent: :destroy
  has_many :comments, through: :posts
  has_one :profile, dependent: :destroy
  belongs_to :organization, optional: true
  has_and_belongs_to_many :roles

  # Validations
  validates :email, presence: true, uniqueness: { case_sensitive: false },
                    format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :name, presence: true, length: { minimum: 2, maximum: 100 }
  validates :age, numericality: { greater_than: 0, allow_nil: true }

  # Callbacks
  before_save :normalize_email
  after_create :send_welcome_email

  # Scopes
  scope :active,    -> { where(active: true) }
  scope :admins,    -> { where(role: 'admin') }
  scope :recent,    -> { order(created_at: :desc) }

  # Enum
  enum status: { pending: 0, active: 1, suspended: 2 }

  private

  def normalize_email
    self.email = email.downcase.strip
  end
end

# Queries
User.all
User.find(1)
User.find_by(email: 'alice@example.com')   # nil if not found
User.find_by!(email: '...')               # raises if not found
User.where(active: true).order(:name).limit(10)
User.where('age > ?', 18)
User.where(age: 18..65)
User.joins(:posts).where(posts: { published: true })
User.includes(:posts, :profile)            # eager load — avoid N+1
User.count
User.pluck(:email)                         # ["alice@...", "bob@..."] — no model objects

Migrations & Database

# Generate migration
# rails generate migration AddPublishedToPosts published:boolean:index

class AddPublishedToPosts < ActiveRecord::Migration[7.2]
  def change
    add_column :posts, :published, :boolean, default: false, null: false
    add_index  :posts, :published
    add_column :posts, :published_at, :datetime

    # Other common helpers:
    # add_reference :posts, :category, null: false, foreign_key: true
    # rename_column :users, :username, :login
    # remove_column :users, :legacy_field
    # change_column_null :users, :email, false
  end
end

# Run migrations
# rails db:migrate
# rails db:migrate:status
# rails db:rollback STEP=2
# rails db:schema:load   # recreate from schema.rb (faster for fresh setup)

# Seeds (db/seeds.rb)
User.create!(name: 'Admin', email: 'admin@example.com', role: 'admin')
10.times { User.create!(Faker::User.safe_message) }
Ruby

Rails Advanced

Ruby: Rails Advanced Service Objects Service objects encapsulate business logic that doesn't belong in models or controllers. They keep controllers thin and mod

Ruby: Rails Advanced

Service Objects

Service objects encapsulate business logic that doesn't belong in models or controllers. They keep controllers thin and models focused on persistence.

# app/services/create_post_service.rb
class CreatePostService
  Result = Struct.new(:success?, :post, :errors, keyword_init: true)

  def initialize(user:, params:)
    @user = user
    @params = params
  end

  def call
    post = @user.posts.build(@params)
    post.slug = SlugGenerator.call(post.title)

    if post.save
      NotifyFollowersJob.perform_later(post)
      Result.new(success?: true, post: post, errors: [])
    else
      Result.new(success?: false, post: post, errors: post.errors.full_messages)
    end
  end
end

# In controller
result = CreatePostService.new(user: current_user, params: post_params).call
if result.success?
  redirect_to result.post
else
  @errors = result.errors
  render :new
end

Background Jobs (Active Job + Sidekiq)

# Gemfile
# gem 'sidekiq'

# config/application.rb
config.active_job.queue_adapter = :sidekiq

# app/jobs/send_welcome_email_job.rb
class SendWelcomeEmailJob < ApplicationJob
  queue_as :default
  retry_on Net::SMTPError, wait: :polynomially_longer, attempts: 5
  discard_on ActiveRecord::RecordNotFound

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

# Enqueue
SendWelcomeEmailJob.perform_later(user.id)          # async
SendWelcomeEmailJob.set(wait: 1.hour).perform_later(user.id)
SendWelcomeEmailJob.perform_now(user.id)            # sync (testing)

# config/sidekiq.yml
# :queues:
#   - [critical, 3]
#   - [default, 2]
#   - [low, 1]

# sidekiq -C config/sidekiq.yml

ActionMailer

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  default from: 'noreply@myapp.com'

  def welcome(user)
    @user = user
    @login_url = login_url
    mail(to: @user.email, subject: 'Welcome to MyApp!')
  end

  def password_reset(user, token)
    @user = user
    @reset_url = password_reset_url(token: token)
    mail(to: @user.email, subject: 'Reset your password')
  end
end

# app/views/user_mailer/welcome.html.erb
# <h1>Hi <%= @user.name %></h1>

# Sending
UserMailer.welcome(@user).deliver_later   # via Active Job (async)
UserMailer.welcome(@user).deliver_now     # sync

Caching

# config/environments/production.rb
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
config.action_controller.perform_caching = true

# Fragment caching in views (Russian doll — nested caches)
# <%= cache @post do %>
#   <h1><%= @post.title %></h1>
#   <% @post.comments.each do |comment| %>
#     <%= cache comment do %>
#       <%= comment.body %>
#     <% end %>
#   <% end %>
# <% end %>

# Low-level caching in Ruby code
class PostsController < ApplicationController
  def expensive_stats
    @stats = Rails.cache.fetch('posts/stats', expires_in: 1.hour) do
      Post.expensive_calculation   # only runs on cache miss
    end
  end
end

# Action caching (whole response) — use HTTP caching instead
# expires_in 1.hour, public: true
# fresh_when(@post)   # ETag + Last-Modified

ActionCable (WebSockets)

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end

  def unsubscribed
    # cleanup
  end

  def speak(data)
    ActionCable.server.broadcast("chat_#{params[:room]}", {
      message: data['message'],
      user: current_user.name
    })
  end
end

# Broadcast from anywhere (e.g., a job)
ActionCable.server.broadcast('chat_general', { message: 'Hello!' })

# JavaScript client
# const cable = createConsumer()
# const channel = cable.subscriptions.create(
#   { channel: 'ChatChannel', room: 'general' },
#   { received(data) { appendMessage(data) } }
# )
Ruby

Concurrency & Performance

Ruby: Concurrency & Performance The GIL (Global Interpreter Lock) MRI Ruby (the standard implementation) has a Global VM Lock (GVL). Only one thread runs Ruby c

Ruby: Concurrency & Performance

The GIL (Global Interpreter Lock)

MRI Ruby (the standard implementation) has a Global VM Lock (GVL). Only one thread runs Ruby code at a time. I/O operations release the GVL, so threads still help with I/O-bound work. For CPU-bound parallelism: use Ractors (Ruby 3+), multiple processes, or JRuby/TruffleRuby.

Threads

# Threads share memory — good for I/O-bound concurrency
threads = (1..5).map do |i|
  Thread.new do
    response = Net::HTTP.get(URI("https://api.example.com/item/#{i}"))
    JSON.parse(response)
  end
end

results = threads.map(&:join).map(&:value)   # wait and collect results

# Mutex — protect shared state
mutex = Mutex.new
counter = 0

threads = 10.times.map do
  Thread.new do
    1000.times do
      mutex.synchronize { counter += 1 }  # atomic increment
    end
  end
end
threads.each(&:join)
puts counter  # 10000 — correct with mutex

# Thread-local variables
Thread.current[:request_id] = SecureRandom.uuid
Thread.current[:request_id]   # only visible to this thread

Fibers

# Fibers — cooperative (not preemptive) concurrency, very lightweight
# Control passes explicitly via Fiber.yield and fiber.resume

producer = Fiber.new do
  5.times do |i|
    puts "Producing #{i}"
    Fiber.yield i              # pause, return value to caller
  end
  nil
end

loop do
  value = producer.resume     # resume from where it yielded
  break if value.nil?
  puts "Consumed #{value}"
end

# Enumerator uses Fibers internally
enum = Enumerator.new do |yielder|
  yielder << 1
  yielder << 2
  yielder << 3
end

enum.next  # 1
enum.next  # 2
enum.next  # 3

Ractors (Ruby 3+)

# Ractors — true parallelism without GVL
# Ractors cannot share mutable objects (enforced at runtime)
# Communicate via message passing

r = Ractor.new do
  msg = Ractor.receive    # block until message arrives
  msg.upcase
end

r.send("hello")
puts r.take              # "HELLO"

# Parallel processing with a worker pool
workers = 4.times.map do
  Ractor.new do
    loop do
      job = Ractor.receive
      Ractor.yield job * 2
    end
  end
end

results = [1, 2, 3, 4].map.with_index do |n, i|
  workers[i % workers.size].send(n)
  workers[i % workers.size].take
end

Performance Tips

  • Frozen string literals: add # frozen_string_literal: true to every file — eliminates string allocation for all literals

  • Avoid N+1 queries: use includes(), eager_load(), or preload() in ActiveRecord. Use Bullet gem in development to detect N+1s automatically.

  • Use pluck() instead of map(&:attr) — returns raw values without instantiating models

  • Database indexes: index all foreign keys, frequently queried columns, and uniqueness constraints

  • Benchmark before optimizing: use the Benchmark module or benchmark-ips gem to measure before changing anything

  • Memory profiling: memory_profiler gem — find objects that are allocated but never freed

  • rack-mini-profiler: shows per-request SQL queries, timing, and memory in the browser

  • Sidekiq over Delayed::Job: Redis-backed, multi-threaded, much faster for background jobs

require 'benchmark/ips'

Benchmark.ips do |x|
  x.report('string concat') { "Hello" + " " + "World" }
  x.report('interpolation') { "Hello #{"World"}" }
  x.report('frozen')        { +"Hello".freeze }
  x.compare!
end

# Check memory allocation
require 'memory_profiler'
report = MemoryProfiler.report do
  1000.times { User.all.to_a }
end
report.pretty_print

Keep your Ruby knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever