← Tech Guides Index
R-01

Quick Reference Decision Matrix

Ruby is a language designed for programmer happiness. This matrix gives you the pattern first, then the 12 sections that follow explain why. If you're coming from Python, the rosetta stone at the bottom bridges the gap.

"I want to..." Decision Matrix

I want to... Use this pattern See Section
Store key-value data Hash / Symbol keys R-03
Transform a collection .map, .select, .reject R-04
Define reusable behavior Module + include R-06
Share code via mixin Module (include/extend) R-06
Handle errors gracefully begin/rescue/ensure R-08
Add dynamic methods define_method / method_missing R-07
Manage dependencies Bundler + Gemfile R-09
Test my code RSpec (or Minitest) R-10
Build a web app Rails (MVC framework) R-12
Debug interactively Pry / debug gem R-08
Define a simple value object Data.define (Ruby 3.2+) R-06
Iterate with index .each_with_index / .each.with_index R-04

Essential Commands

Command Purpose
ruby script.rb Run a Ruby file
irb Interactive Ruby shell
gem install NAME Install a gem
bundle install Install Gemfile dependencies
bundle exec CMD Run command in bundle context
rails new APP Create new Rails app
rails server Start dev server (port 3000)
rails console Rails REPL with app loaded
rake TASK Run a Rake task
rspec Run RSpec test suite
rubocop Lint and auto-correct
ruby -e 'code' Execute one-liner

Ruby vs Python Rosetta Stone

Ruby

Print to stdout

puts "hello"

Python

Print to stdout

print("hello")

Ruby

String interpolation

"Hello #{name}"

Python

String interpolation

f"Hello {name}"

Ruby

Transform a list

[1, 2, 3].map { |x| x * 2 }

Python

Transform a list

[x * 2 for x in [1, 2, 3]]

Ruby

Hash / Dictionary

{ name: "Ruby" }

Python

Hash / Dictionary

{"name": "Ruby"}

Ruby

Blocks / Loops

5.times { |i| puts i }

Python

Blocks / Loops

for i in range(5): print(i)

Ruby

Class attributes

attr_accessor :name

Python

Class attributes

self.name = name

Ruby

Nil check

value.nil?

Python

Nil check

value is None

Ruby

Ternary expression

x > 0 ? "pos" : "neg"

Python

Ternary expression

"pos" if x > 0 else "neg"

Ruby

Modules / Mixins

include Enumerable

Python

Modules / Mixins

from collections.abc import Iterable
↑ Back to Top
R-02

Ruby Philosophy & Design

Ruby is designed to make programmers happy. Every design decision traces back to Matz's core belief that developer joy matters more than machine optimization.

I hope to see Ruby help every programmer in the world to be productive, to enjoy programming, and to be happy. That is the primary purpose of the Ruby language. Yukihiro "Matz" Matsumoto

Core Principles

Programmer Happiness

Ruby optimizes for developer joy over raw performance. The language reads almost like English. Method names are verbs, predicates end in ?, and dangerous methods end in !.

Everything is an Object

There are no primitives. Every value has methods, a class, and responds to messages.

5.times { puts "hello" } nil.class # => NilClass true.is_a?(Object) # => true

Principle of Least Surprise

Methods behave the way you'd expect. Naming is intuitive and consistent across the standard library.

"hello".reverse # => "olleh" "hello".upcase # => "HELLO" [3,1,2].sort # => [1, 2, 3]

Multiple Ways

Unlike Python's "one obvious way," Ruby embraces multiple approaches. Flexibility over uniformity. Use unless or if !. Use {} or do...end.

Duck Typing

"If it walks like a duck and quacks like a duck, it's a duck." Ruby cares about what objects can do, not what they are. No interfaces needed.

Blocks Everywhere

First-class closures make Ruby uniquely expressive. Every method can accept a block.

[1,2,3].select { |n| n.odd? } # => [1, 3]
Ruby inherited the Perl philosophy of having more than one way to do the same thing. I inherited that philosophy from Larry Wall, who is my hero actually. Matz

Mental Model Comparison

Concept Ruby Python JavaScript
Philosophy Programmer happiness One obvious way Flexibility + ecosystem
Typing Dynamic, strong, duck Dynamic, strong, duck Dynamic, weak
Everything is... An object An object (mostly) Prototype-based
Blocks/closures Built-in (yield) No equivalent Arrow functions
Nil/null nil (NilClass) None (NoneType) null + undefined
Package mgr Bundler + gem pip + uv npm / yarn / pnpm
REPL irb / pry python / ipython node
Style guide RuboCop PEP 8 / Ruff ESLint / Prettier
Web framework Rails Django / FastAPI Express / Next.js

30+
Years — First released 1995
180K+
Gems on RubyGems.org
#1
Programmer happiness rating
3.3+
Latest stable with YJIT
↑ Back to Top
R-03

Core Syntax & Types

Ruby's syntax reads like poetry — minimal punctuation, optional parentheses, and expressive method names. Every value is an object, from integers to nil.

Variable Types

Prefix Scope Example Notes
(none) Local name = "Ruby" Block/method scope
@ Instance @name Per-object state
@@ Class @@count Shared across instances (avoid)
$ Global $LOAD_PATH App-wide (use sparingly)
UPPER Constant MAX_SIZE = 100 Convention, not enforced (warning on reassign)

Core Types

Integer

Arbitrary precision. No overflow.

42.even? # => true 2 ** 100 # big integers work 1_000_000 # underscores for clarity

Float

IEEE 754 double-precision. Use BigDecimal for money.

3.14.round(1) # => 3.1 0.1 + 0.2 # => 0.30000...04

String

Mutable by default. Double quotes enable interpolation.

"hello".length # => 5 "hello" << "!" # mutation "hello".freeze # immutable

Symbol

Immutable, interned identifiers. Fast comparison.

:name.class # => Symbol :name.to_s # => "name" :name.object_id == :name.object_id

Array

Ordered, integer-indexed, heterogeneous collection.

[1, "two", :three] %w[foo bar baz] # => ["foo","bar","baz"] [1,2,3].last # => 3

Hash

Key-value store. Ordered since Ruby 1.9.

{ name: "Matz", lang: "Ruby" } h.fetch(:name) # safe access h.dig(:a, :b) # nested access

Range

Represents an interval. Lazy-evaluable.

(1..5).to_a # => [1, 2, 3, 4, 5] (1...5).to_a # => [1, 2, 3, 4] ('a'..'z') # works with strings

NilClass

The absence of value. Only nil and false are falsy.

nil.nil? # => true nil.to_s # => "" nil.to_i # => 0

TrueClass / FalseClass

Boolean singletons. Only nil and false are falsy; 0 and "" are truthy.

true.class # => TrueClass !!0 # => true (0 is truthy!) !!"" # => true ("" is truthy!)

Regexp

First-class regular expressions with =~ operator.

"hello" =~ /e(ll)o/ $1 # => "ll" (capture group) "hello".match?(/ell/) # => true

String Interpolation

Strings — Double vs Single Quotes
name = "Ruby" version = 3.3 puts "Welcome to #{name} #{version}" # Welcome to Ruby 3.3 puts 'No interpolation: #{name}' # No interpolation: #{name} puts "Math: #{2 + 2}" # Math: 4

Symbol vs String Decision Guide

Do — Symbol

Use for identifiers, hash keys, flags. Symbols are immutable, interned, and fast to compare.

:name, :status, :admin

Avoid — String as Hash Key

Use strings only when keys come from external input (JSON, CSV, user params).

"name", "status", "admin"
Symbols vs Strings in Practice
# Symbols -- immutable, interned, fast comparison status = :active user = { name: "Matz", role: :admin } # Strings -- mutable, for text content greeting = "Hello, world" greeting << "!" # mutation

Essential Array & Hash Methods

Method Array Example Result
.map [1,2,3].map { |n| n * 2 } [2, 4, 6]
.select [1,2,3,4].select(&:even?) [2, 4]
.reject [1,2,3,4].reject(&:even?) [1, 3]
.find [1,2,3].find { |n| n > 1 } 2
.reduce [1,2,3].reduce(0, :+) 6
.flat_map [[1,2],[3]].flat_map { |a| a } [1, 2, 3]
.dig {a: {b: 1}}.dig(:a, :b) 1
.merge {a: 1}.merge(b: 2) {a: 1, b: 2}
.tally ["a","b","a"].tally {"a"=>2, "b"=>1}
.group_by [1,2,3].group_by(&:odd?) {true=>[1,3], false=>[2]}
Tip: Enable frozen string literals at the top of your files with # frozen_string_literal: true. This makes all string literals frozen by default, preventing accidental mutation and improving performance.

Type Conversion

Explicit Conversions
# Explicit conversions "42".to_i # => 42 "3.14".to_f # => 3.14 42.to_s # => "42" 42.to_f # => 42.0 [1, 2].to_s # => "[1, 2]" { a: 1 }.to_a # => [[:a, 1]] # Kernel conversion methods (strict) Integer("42") # => 42 Integer("nope") # => ArgumentError (safer!) Float("3.14") # => 3.14
↑ Back to Top
R-04

Control Flow & Iteration

Ruby's control flow reads like English prose. Postfix conditionals, the unless keyword, and a rich iterator library make loops rare — you almost never write a traditional for loop in Ruby.

Conditional Patterns

Conditionals
# Standard if/elsif/else if score >= 90 grade = "A" elsif score >= 80 grade = "B" else grade = "C" end # Postfix (inline) conditional puts "Adult" if age >= 18 # unless (negative if) raise "Invalid" unless valid? # Ternary status = logged_in? ? :active : :guest # case/when (like switch) case role when :admin then "Full access" when :editor then "Edit access" when :viewer then "Read only" else "No access" end

Ruby 3 Pattern Matching

Structural Pattern Matching (Ruby 3.0+)
# case/in -- structural pattern matching (Ruby 3.0+) case response in { status: 200, body: String => body } puts "Success: #{body}" in { status: 404 } puts "Not found" in { status: (500..) } puts "Server error" end # Find pattern case [1, 2, 3, 4, 5] in [*, 3, *post] puts "Found 3, followed by: #{post}" end # Pin operator expected = 42 case value in ^expected puts "Matched!" end

Iteration Decision Flow

What are you iterating?

Start here. The answer determines which iterator to use.

Array or Range

.each, .map, .select — the big three for collections.

Hash

.each { |k, v| }, .transform_values, .filter_map

Need the index?

.each_with_index (0-based) or .each.with_index(1) (custom start)

Building an object?

.each_with_object([]) or .each_with_object({})

N times / counting

5.times { }, 1.upto(10) { }, 10.downto(1) { }


Complete Iterator Reference

Method Purpose Example Returns
.each Side effects [1,2].each { |n| puts n } Original array
.map Transform [1,2].map { |n| n * 10 } [10, 20]
.select Filter (keep) (1..10).select(&:even?) [2,4,6,8,10]
.reject Filter (remove) (1..10).reject(&:even?) [1,3,5,7,9]
.find First match [3,7,2].find { |n| n > 5 } 7
.reduce Accumulate [1,2,3].reduce(:+) 6
.flat_map Map + flatten [[1],[2,3]].flat_map { |a| a } [1, 2, 3]
.group_by Group %w[hi hey ho].group_by { |w| w[0] } {"h"=>["hi","hey","ho"]}
.tally Count %w[a b a c a].tally {"a"=>3,"b"=>1,"c"=>1}
.each_with_object Build [1,2].each_with_object([]) { |n,a| a << n*2 } [2, 4]
.zip Pair up [1,2].zip([3,4]) [[1,3],[2,4]]
.each_slice Chunk (1..6).each_slice(2).to_a [[1,2],[3,4],[5,6]]
.any? Check [1,2,3].any? { |n| n > 2 } true
.all? Check all [2,4,6].all?(&:even?) true
.none? Check none [1,3,5].none?(&:even?) true
.count Count matches [1,2,3,4].count(&:even?) 2
.min_by / .max_by Extremes %w[hi hello hey].min_by(&:length) "hi"
.sort_by Sort %w[banana apple cherry].sort_by(&:length) ["apple","banana","cherry"]
.sum Sum [1,2,3].sum 6

Traditional Loop vs Ruby Idiom

Do — Ruby Idiom

# Transform doubled = numbers.map { |n| n * 2 } # Filter adults = users.select { |u| u.age >= 18 } # Accumulate total = orders.sum(&:amount)

Avoid — Traditional Loop

# Don't do this in Ruby doubled = [] for n in numbers doubled << n * 2 end adults = [] numbers.each do |u| adults << u if u.age >= 18 end
Tip: Use &:method_name shorthand when a block just calls one method: [1,-2,3].select(&:positive?) is cleaner than [1,-2,3].select { |n| n.positive? }.

Flow Control in Blocks

return, next, break
# return -- exits the ENCLOSING METHOD (not just the block!) def find_first_admin(users) users.each do |user| return user if user.admin? # returns from find_first_admin end nil end # next -- skip to next iteration (like continue) [1, 2, 3, 4].each do |n| next if n.even? puts n # prints 1, 3 end # break -- exit the loop entirely result = [1, 2, 3, 4].each do |n| break n if n > 2 # result = 3 end
↑ Back to Top
R-05

Methods & Blocks

Methods in Ruby are remarkably flexible — optional parentheses, default values, keyword arguments, and the ? and ! naming conventions. But the real power lies in blocks: Ruby’s built-in closure mechanism that makes the language uniquely expressive.

Method Definition Patterns

Defining Methods — Parameters & Conventions
# Basic method def greet(name) "Hello, #{name}!" end # Default parameters def connect(host, port: 3000, ssl: true) # keyword args with defaults end # Splat (variable args) def log(*messages) messages.each { |msg| puts msg } end # Double splat (keyword collector) def configure(**options) options.each { |key, val| puts "#{key}: #{val}" } end # ? convention -- returns boolean def valid? errors.empty? end # ! convention -- dangerous/mutating def save! raise "Invalid" unless valid? persist end

Naming Conventions

Pattern Convention Example
method_name Snake case (always) find_user, total_amount
method? Returns boolean empty?, valid?, admin?
method! Dangerous / mutates save!, sort!, gsub!
self.method Class method User.find(1)
_unused Intentionally unused _, _index

Blocks & Yield

Blocks — Ruby’s Built-in Closures
# Blocks are closures passed to methods def with_logging puts "Starting..." result = yield # execute the block puts "Done!" result end with_logging { User.count } # Multi-line block (use do...end) with_logging do users = User.active users.count end # Yield with arguments def transform(value) yield(value) if block_given? end transform(5) { |n| n * 2 } # => 10

Block vs Proc vs Lambda

Feature Block Proc Lambda
Object? No (syntax) Yes Yes (special Proc)
Create { } or do...end Proc.new { } -> { } or lambda { }
Arity Loose Loose Strict (raises ArgumentError)
return behavior Returns from enclosing method Returns from enclosing method Returns from lambda only
Call syntax yield proc.call or proc.() lambda.call or lambda.()
Common use Iteration, callbacks Stored blocks, DSLs Functional programming

Practical Example — Building Your Own .map

Reimplementing .map with yield
def my_map(array) result = [] array.each { |item| result << yield(item) } result end my_map([1, 2, 3]) { |n| n ** 2 } # => [1, 4, 9]
Tip: Prefer { } for single-line blocks and do...end for multi-line blocks. This is a strong Ruby convention: users.map { |u| u.name } vs users.each do |u| ... end.

Closures & Method References

Blocks Capture Surrounding Scope
# Blocks capture surrounding scope multiplier = 3 [1, 2, 3].map { |n| n * multiplier } # => [3, 6, 9] # Method objects method_ref = method(:puts) [1, 2, 3].each(&method_ref) # Converting block to proc with & def capture(&block) block.call # block is now a Proc object end
↑ Back to Top
R-06

Object-Oriented Ruby

Ruby is purely object-oriented — everything is an object, including numbers and nil. Ruby’s object model is elegant: single inheritance with powerful module mixins, duck typing, and a method lookup chain that’s worth understanding deeply.

Class Anatomy

A Complete Ruby Class
class User attr_accessor :name, :email # getter + setter attr_reader :id # getter only # Class-level state @@count = 0 # Constructor def initialize(name, email) @name = name @email = email @id = SecureRandom.uuid @@count += 1 end # Instance method def display "#{@name} <#{@email}>" end # Class method def self.count @@count end # Private methods private def secret_token Digest::SHA256.hexdigest(@email) end end

Ruby Object Model — Method Lookup Chain

BasicObject

Bare minimum (only 8 methods). Parent of everything.

Kernel module

Included in Object. Gives puts, require, raise, block_given?

Object

Default parent for all classes. Includes Kernel module.

YourClass

Your classes inherit from Object by default.

Tip: When you write class Dog; end, Ruby automatically does class Dog < Object. Every class you create is part of this lookup chain.

Include vs Extend vs Prepend

Mechanism What it does Methods become Use when
include Adds module to ancestors Instance methods Sharing behavior across classes
extend Adds module methods to object Class (singleton) methods Adding class-level helpers
prepend Inserts module BEFORE class Instance methods (override) Wrapping/decorating methods
Include vs Extend in Action
module Loggable def log(msg) puts "[#{self.class}] #{msg}" end end class User include Loggable # instance method end User.new.log("created") # => [User] created class Admin extend Loggable # class method end Admin.log("initialized") # => [Admin] initialized

If it walks like a duck and quacks like a duck, then it’s a duck. — Ruby doesn’t care about types. It cares about capabilities. If an object responds to the method you’re calling, it works.
Duck Typing in Practice
# Ruby doesn't care about the class -- just the interface def print_length(obj) puts obj.length # works on String, Array, Hash... end print_length("hello") # => 5 print_length([1, 2, 3]) # => 3 print_length({a: 1}) # => 1 # Check capability with respond_to? def safe_length(obj) obj.respond_to?(:length) ? obj.length : "N/A" end

Practical Patterns — Struct, Data, Comparable

Struct Quick Classes

Point = Struct.new(:x, :y) p = Point.new(1, 2) p.x # => 1

Mutable. Auto-generates ==, to_a, members.

Data Ruby 3.2+

Point = Data.define(:x, :y) p = Point.new(x: 1, y: 2) p.x # => 1 (frozen!)

Immutable. Keyword-only constructor. Preferred for value objects.

Comparable Mixin

class Box include Comparable attr_accessor :volume def <=>(other) volume <=> other.volume end end

Define <=> and get <, >, ==, between? for free.

Gotcha: Avoid @@class_variables — they’re shared across the entire inheritance hierarchy, not just the defining class. Use class-level instance variables (@var inside self methods) instead.
↑ Back to Top
R-07

Metaprogramming Essentials

Metaprogramming is Ruby’s superpower and its footgun. Code that writes code enables elegant DSLs like Rails’ ActiveRecord, but it can also create impenetrable spaghetti. Use it deliberately, sparingly, and always with tests.

method_missing + respond_to_missing?

Dynamic Method Dispatch
class FlexibleConfig def initialize @data = {} end def method_missing(name, *args) if name.to_s.end_with?("=") @data[name.to_s.chomp("=")] = args.first else @data[name.to_s] end end def respond_to_missing?(name, include_private = false) true end end config = FlexibleConfig.new config.database = "myapp" config.database # => "myapp"
Gotcha: ALWAYS define respond_to_missing? alongside method_missing. Without it, respond_to? returns false even though the method works, breaking duck typing contracts.

define_method

Generating Methods at Load Time
class User ROLES = [:admin, :editor, :viewer] ROLES.each do |role| define_method("#{role}?") do @role == role end end end user = User.new user.admin? # dynamically defined method

Open Classes & Refinements

Do — Refinements (Safe Monkey Patching)

module StringExtensions refine String do def shout upcase + "!!!" end end end using StringExtensions "hello".shout # => "HELLO!!!" # Only active in this file/scope

Avoid — Monkey Patching

# DON'T do this globally class String def shout upcase + "!!!" end end # Affects ALL strings everywhere # Can break gems and core library

“Should I Metaprogram?” Decision Flow

Is the pattern repeated 3+ times?

No → Write normal code. Don’t over-engineer.

Yes — Will a simple loop/array work?

Yes → Use .each + define_method. Keep it simple.

No — Do you need runtime flexibility?

No → Generate code at load time with class-level loops.

Yes — Are you building a DSL?

Yes → Consider instance_eval / class_eval.

No?

Think twice. method_missing is usually overkill.


DSL Building Example

Building a Router DSL with instance_eval
class Route attr_reader :routes def initialize(&block) @routes = [] instance_eval(&block) end def get(path, to:) @routes << { method: :get, path: path, to: to } end def post(path, to:) @routes << { method: :post, path: path, to: to } end end routes = Route.new do get "/users", to: "users#index" post "/users", to: "users#create" get "/users/:id", to: "users#show" end
Tip: Rails’ ActiveRecord is the canonical Ruby metaprogramming success story. has_many :posts, validates :email, presence: true, and scope :active, -> { where(active: true) } are all metaprogramming — they read like configuration but generate methods behind the scenes.
↑ Back to Top
R-08

Error Handling & Debugging

Ruby’s exception system is clean and predictable. The hierarchy matters: rescue StandardError (the default), not Exception. And when debugging, Pry turns your error into an interactive playground.

Exception Hierarchy

Ruby’s Exception Class Tree
Exception |-- NoMemoryError |-- ScriptError | |-- LoadError | `-- SyntaxError |-- SignalException | `-- Interrupt `-- StandardError ← rescue catches this by default |-- ArgumentError |-- IOError |-- NameError | `-- NoMethodError |-- RangeError |-- RuntimeError ← raised by 'raise "msg"' |-- TypeError |-- ZeroDivisionError `-- StopIteration
Gotcha: NEVER rescue Exception — it catches Interrupt (Ctrl+C), NoMemoryError, and SyntaxError. Always rescue StandardError (the default) or a specific subclass.

The Full begin/rescue/else/ensure Pattern

Complete Error Handling Pattern
begin file = File.open("data.csv") data = file.read process(data) rescue Errno::ENOENT => e puts "File not found: #{e.message}" rescue CSV::MalformedCSVError => e puts "Bad CSV: #{e.message}" retry if @retries < 3 # retry the begin block rescue StandardError => e puts "Unexpected: #{e.class} -- #{e.message}" raise # re-raise after logging else puts "Success! No errors." ensure file&.close # always runs (like finally) end

Rescue Patterns Reference

Pattern When to use Example
rescue SpecificError Known failure mode rescue ActiveRecord::RecordNotFound
rescue => e Catch StandardError + inspect rescue => e; log(e)
retry Transient failures Network timeouts, rate limits
raise in rescue Re-raise after logging rescue => e; notify(e); raise
Inline rescue Quick fallback (use sparingly) value = risky_call rescue default
ensure Cleanup (close files, connections) Always runs regardless of errors

Good vs Bad Error Handling

Do — Rescue Specific Errors

# Rescue specific errors def fetch_user(id) User.find(id) rescue ActiveRecord::RecordNotFound nil end # Custom exceptions for your domain class InsufficientFundsError < StandardError; end def withdraw(amount) raise InsufficientFundsError if amount > balance @balance -= amount end

Avoid — Silent or Overbroad Rescue

# Too broad -- swallows everything begin do_something rescue nil # silent failure! end # Rescuing Exception catches signals begin do_something rescue Exception => e # catches Ctrl+C! log(e) end

Custom Exception Classes

Domain-Specific Exception Hierarchy
# Define domain-specific exceptions class AppError < StandardError; end class AuthenticationError < AppError; end class AuthorizationError < AppError; end class PaymentError < AppError attr_reader :code def initialize(message, code: nil) super(message) @code = code end end raise PaymentError.new("Declined", code: "card_declined")

Debugging Tools

Pry Interactive REPL

Drop binding.pry anywhere to open an interactive debugger.

  • ls — list methods/variables
  • cd — step into objects
  • show-source — view method source
  • whereami — show current context

debug gem Ruby 3.1+

Built-in debugger, no gem install required.

  • binding.break or debugger
  • step / next / continue
  • info / backtrace
  • Ships with Ruby — zero setup

puts debugging Old Reliable

Sometimes the simplest tool is the best one.

  • puts variable.inspect
  • p variable (shorthand for puts + inspect)
  • pp variable (pretty print)
  • warn "debug: #{var}" (goes to stderr)

Pry Cheat Sheet

Command Action
binding.pry Insert breakpoint
ls List methods on current scope
ls SomeClass List methods on class
cd object Step into object context
cd .. Step back out
show-source method_name View source code
show-doc method_name View documentation
whereami Show surrounding code
wtf? Show last exception backtrace
exit Continue execution
↑ Back to Top
R-09

Gems & Bundler

Gems are Ruby’s package system — there are over 180,000 on RubyGems.org. Bundler manages your project’s gem dependencies with a lockfile, ensuring consistent environments across development, CI, and production.

Bundler Workflow

bundle init

Creates a Gemfile

Edit Gemfile

Add your gems

bundle install

Downloads gems, creates Gemfile.lock

bundle exec

Run commands in bundle context

Commit both files

Commit both Gemfile AND Gemfile.lock


Gemfile Syntax

Gemfile — Declaring Dependencies
source "https://rubygems.org" ruby "3.3.0" # Main dependencies gem "rails", "~> 8.0" gem "puma", ">= 6.0" gem "propshaft" # Groups group :development, :test do gem "rspec-rails" gem "factory_bot_rails" gem "pry-rails" end group :development do gem "rubocop", require: false gem "solargraph", require: false end group :production do gem "redis" gem "sidekiq" end # Git source gem "my_engine", git: "https://github.com/org/engine.git", branch: "main"

Version Constraints

Operator Meaning Example Matches
= Exact gem "rails", "= 8.0.0" Only 8.0.0
>= Minimum gem "puma", ">= 6.0" 6.0, 6.1, 7.0...
~> Pessimistic (recommended) gem "rails", "~> 8.0" >= 8.0, < 9.0
~> Patch-level gem "pg", "~> 1.5.4" >= 1.5.4, < 1.6
>=, < Range gem "ruby-lsp", ">= 0.5", "< 1.0" 0.5 to 0.99

Essential Gems

Rails

Full-stack web framework. The Ruby ecosystem’s center of gravity.

RSpec

BDD testing framework. The most popular way to test Ruby.

Puma

Multi-threaded web server. Default for Rails.

Sidekiq

Background job processor. Redis-backed, battle-tested.

Devise

Authentication. Handles login, registration, password resets.

Pundit

Authorization. Policy objects for clean access control.

RuboCop

Linter + formatter. Enforces style guide rules.

Pry

Enhanced REPL and debugger. Drop-in IRB replacement.

FactoryBot

Test data generator. Replaces fixtures for most teams.


Bundle Commands

Command Purpose
bundle init Create new Gemfile
bundle install Install gems from Gemfile
bundle update GEM Update specific gem
bundle update Update all gems (careful!)
bundle exec CMD Run command in bundle context
bundle add GEM Add gem to Gemfile + install
bundle outdated List gems with newer versions
bundle info GEM Show gem details and path
bundle open GEM Open gem source in editor
bundle audit Security vulnerability check (gem install bundler-audit)
Tip: Always commit your Gemfile.lock. It ensures every developer and CI run uses the exact same gem versions. The only exception: gem libraries (not apps) should NOT commit Gemfile.lock.
↑ Back to Top
R-10

Testing with RSpec & Minitest

Ruby has a strong testing culture — “if it’s not tested, it’s broken” is the community ethos. RSpec dominates Rails projects with its expressive DSL, while Minitest ships with Ruby for a lightweight alternative.

RSpec vs Minitest

Feature RSpec Minitest
Style BDD / describe blocks xUnit / def test_ or spec
Ships with Ruby No (gem) Yes (stdlib)
DSL complexity Rich (describe, context, let) Minimal
Community share ~75% of Rails apps ~25%
Mocking built-in Yes (doubles, stubs) Yes (basic)
Matchers 30+ built-in assert_* methods
Speed Slightly slower Slightly faster
Best for Rails apps, team projects Libraries, minimal deps

RSpec Anatomy

RSpec — Describe, Context, It
RSpec.describe User do # let = lazy-loaded variable let(:user) { User.new(name: "Matz", email: "matz@ruby.org") } # subject = the thing being tested subject { user } # before = setup before { user.activate! } describe "#display_name" do it "returns formatted name" do expect(user.display_name).to eq("Matz <matz@ruby.org>") end context "when name is nil" do let(:user) { User.new(name: nil, email: "matz@ruby.org") } it "falls back to email" do expect(user.display_name).to eq("matz@ruby.org") end end end describe ".active" do it "returns only active users" do active = User.create!(name: "A", active: true) User.create!(name: "B", active: false) expect(User.active).to contain_exactly(active) end end end

Common RSpec Matchers

Matcher Tests Example
eq Equality expect(x).to eq(5)
be Identity expect(x).to be(true)
be_nil Nil check expect(x).to be_nil
be_truthy / be_falsey Truthiness expect(x).to be_truthy
include Containment expect([1,2]).to include(1)
match_array Array (any order) expect(a).to match_array([2,1])
have_attributes Object attrs expect(u).to have_attributes(name: "Matz")
raise_error Exceptions expect { x }.to raise_error(TypeError)
change State change expect { u.save }.to change(User, :count).by(1)
respond_to Method exists expect(u).to respond_to(:name)
be_a / be_an Type check expect(x).to be_a(String)
start_with / end_with String/array expect("hello").to start_with("hel")
match Regex expect(s).to match(/\d+/)

Mocking & Stubbing

Test Doubles, Stubs & Expectations
# Test doubles let(:mailer) { instance_double(UserMailer) } # Stubbing allow(UserMailer).to receive(:welcome).and_return(mailer) allow(mailer).to receive(:deliver_later) # Expectations (verify it was called) expect(UserMailer).to have_received(:welcome).with(user) # Spy pattern mailer = spy("mailer") user.send_welcome(mailer) expect(mailer).to have_received(:deliver)

Same Test in Both Frameworks

RSpec
RSpec.describe Calculator do describe "#add" do it "sums two numbers" do calc = Calculator.new expect(calc.add(2, 3)).to eq(5) end end end
Minitest
class CalculatorTest < Minitest::Test def test_add_sums_two_numbers calc = Calculator.new assert_equal 5, calc.add(2, 3) end end
Rails: Rails testing layers: Model specs test business logic. Request specs test API endpoints (replace controller specs). System specs test browser interaction with Capybara. Use FactoryBot over fixtures for test data — it’s more maintainable.
↑ Back to Top
R-11

Ruby Tooling & Environment

A well-configured Ruby development environment starts with a version manager and expands to include a powerful REPL, linter, language server, and task runner. Ruby 3.3+ with YJIT enabled delivers impressive performance.

Version Managers

Tool Approach Shell integration Multiple languages Recommendation
rbenv Shims + plugins Lightweight Ruby only Best for Ruby-only devs
rvm Function override Heavy (modifies shell) Ruby only Legacy, avoid for new setups
asdf Plugin-based Moderate Ruby, Node, Python, etc. Best for polyglot devs
mise (formerly rtx) Rust-based, fast Minimal All languages Newest, fastest

IRB vs Pry

Feature IRB (built-in) Pry (gem)
Ships with Ruby Yes No (gem install)
Syntax highlighting Yes (Ruby 3.1+) Yes
Tab completion Basic Advanced
Step into objects No Yes (cd object)
Source viewing No Yes (show-source)
Debugger integration Via debug gem Built-in (binding.pry)
Plugin ecosystem Limited Rich (pry-byebug, etc.)
Auto-indent Yes Yes

Ruby CLI Flags

Flag Purpose Example
-e Execute one-liner ruby -e 'puts 2 + 2'
-w Enable warnings ruby -w script.rb
-c Syntax check only ruby -c script.rb
-v Print version + verbose ruby -v
--yjit Enable YJIT JIT compiler ruby --yjit app.rb
-r Require library before run ruby -rjson -e 'puts JSON.parse("{}")'
-I Add to load path ruby -Ilib script.rb
-n Line-by-line processing ruby -ne 'puts $_.upcase'
Tip: YJIT (Yet another Ruby JIT) in Ruby 3.3 delivers 15–25% speedup on real-world Rails apps. Enable it with --yjit flag or RUBY_YJIT_ENABLE=1. It’s production-ready and used by Shopify at scale.

Rake

Rakefile — Ruby’s Make
# Rakefile namespace :db do desc "Seed the database" task seed: :environment do puts "Seeding..." User.create!(name: "Admin", role: :admin) puts "Done!" end desc "Reset and re-seed" task reset: [:drop, :create, :migrate, :seed] end # Custom task task :stats do puts "Models: #{Dir['app/models/*.rb'].count}" puts "Tests: #{Dir['spec/**/*_spec.rb'].count}" end

RuboCop Configuration

.rubocop.yml — Linter Rules
# .rubocop.yml AllCops: TargetRubyVersion: 3.3 NewCops: enable Exclude: - 'db/schema.rb' - 'vendor/**/*' Style/FrozenStringLiteralComment: Enabled: true Metrics/MethodLength: Max: 15 Layout/LineLength: Max: 120
Tip: Solargraph provides LSP (Language Server Protocol) support for Ruby — autocomplete, go-to-definition, hover docs, and diagnostics in any editor. Install with gem install solargraph and configure your editor’s LSP client.
↑ Back to Top
R-12

Rails Essentials

Ruby on Rails is the framework that made Ruby famous. Rails 8 doubles down on the “one person framework” vision with built-in authentication, deployment with Kamal, and the Solid family replacing external dependencies.

MVC Request Flow

How a Request Flows Through Rails
Browser Request ↓ Router → routes.rb maps URL to controller#action ↓ Controller → Handles request, calls models ↓ ModelActiveRecord: DB queries, validations, logic ↓ Controller → Sets instance variables for view ↓ View → ERB template renders HTML ↓ Response sent to browser

Creating a New Rails App

rails new — Project Generation
# Rails 8 with modern defaults rails new myapp # SQLite + Propshaft + Puma rails new myapp -d postgresql # PostgreSQL rails new myapp --api # API-only (no views) rails new myapp -T # Skip Minitest (use RSpec) rails new myapp --css tailwind # Tailwind CSS

Routing

config/routes.rb — URL Mapping
# config/routes.rb Rails.application.routes.draw do root "pages#home" # RESTful resources resources :users do resources :posts, only: [:index, :create] end # Custom routes get "about", to: "pages#about" post "login", to: "sessions#create" # Namespace namespace :api do namespace :v1 do resources :articles, only: [:index, :show] end end # Health check (Rails 8) get "up", to: "rails/health#show", as: :rails_health_check end

ActiveRecord Essentials

Models — Associations, Validations, Scopes
class User < ApplicationRecord # Associations has_many :posts, dependent: :destroy has_one :profile belongs_to :team has_many :comments, through: :posts # Validations validates :email, presence: true, uniqueness: true validates :name, length: { minimum: 2, maximum: 50 } # Scopes scope :active, -> { where(active: true) } scope :recent, -> { order(created_at: :desc).limit(10) } scope :admins, -> { where(role: "admin") } # Callbacks before_save :normalize_email private def normalize_email self.email = email.downcase.strip end end # Queries User.where(active: true).order(:name) User.find_by(email: "matz@ruby.org") User.includes(:posts).where(posts: { published: true }) User.active.recent.count

Rails Commands

Command Purpose
rails new APP Create new app
rails server / rails s Start dev server
rails console / rails c Interactive console
rails generate model NAME Generate model + migration
rails generate controller NAME Generate controller
rails generate migration NAME Generate migration
rails db:migrate Run pending migrations
rails db:rollback Undo last migration
rails db:seed Run db/seeds.rb
rails routes Show all routes
rails credentials:edit Edit encrypted credentials
rails generate authentication Generate auth system (Rails 8)

Rails 8 Features

Kamal 2

Deploy to any VPS with zero-downtime deploys. Built-in to Rails 8. No PaaS required.

Solid Queue

Database-backed job backend. Replaces Redis + Sidekiq for most apps.

Solid Cache

Database-backed cache store. One less infrastructure dependency.

Solid Cable

Database-backed Action Cable adapter. WebSockets without Redis.

Propshaft

Modern asset pipeline. Replaces Sprockets. Fingerprinting + load paths only.

Thruster

HTTP/2 proxy in front of Puma. Asset compression + caching built-in.

Authentication Generator

rails generate authentication creates a complete auth system.

Kamal Proxy

Zero-downtime deploy proxy. Handles SSL, routing, health checks.

No PaaS Required

Rails 8’s motto: deploy to a $5 VPS and scale from there.


Hotwire Stack

Turbo Drive

Intercepts link clicks and form submissions, swaps <body> content via AJAX. No full page reloads. Zero JavaScript required.

Turbo Frames

Decompose pages into independently-updating frames. Click a link inside a frame, only that frame updates.

Turbo Streams

Deliver partial page updates over WebSocket or HTTP. Append, prepend, replace, remove DOM elements from the server.

Stimulus

Modest JavaScript framework. Attach behavior to HTML with data-controller, data-action, data-target attributes.

Convention over Configuration — Rails makes decisions for you so you can focus on building your product. Default project structure, naming conventions, and sensible defaults mean less bikeshedding and more shipping.
Rails: Rails 8’s “one person framework” vision means a solo developer can build, deploy, and operate a production web application. Kamal deploys to any VPS, Solid Queue/Cache/Cable eliminate Redis, and Thruster eliminates nginx.
↑ Back to Top
GuideRuby Programming
SubjectLanguage & Rails Reference
FormatConceptual Reference
Sections12
Created2026-02-10
VersionRuby 3.3 / Rails 8
StyleUkiyo-e
Number22
StatusActive