Ruby basics for Rails developers: lessons from production
If you're building Rails applications, you've probably noticed that Ruby feels different from most languages. The syntax is cleaner, the philosophy rewards readability, and there's often multiple ways to solve the same problem. Understanding Ruby basics for Rails developers isn't just about syntax—it's about writing code that your team maintains without friction, that scales without surprising you, and that production monitors without keeping you awake at night.
Everything is an object
In Ruby, everything—and we mean everything—is an object. Strings, numbers, arrays, even methods themselves. This isn't a theoretical nicety; it's a practical reality that changes how you write Rails code.
Consider what happens when you work with a simple integer in your Rails controller:
class ProductsController < ApplicationController
def calculate_discount
price = 99
discounted = price.times { |i| puts i } # Works! Integer has .times
tax_multiplier = 1.15
final_price = (price * tax_multiplier).round(2)
end
endThat .times method on an integer, the .round on a float—these are possible because numbers are objects with their own methods. In production systems handling payment calculations, this object-oriented approach often means you're calling well-tested, built-in methods instead of writing custom utility functions. You're not manually iterating a counter; you call 5.times and Ruby handles it. You're not parsing a timestamp manually; you call Time.current.to_i and trust the implementation.
Even nil is an object with behavior:
user = User.find_by(email: "notfound@example.com") # Returns nil
user&.name # Safe navigation operator works because nil is an object
user&.name&.upcase # Chains safely, returns nil if user is nilThis is crucial in Rails apps. We use the safe navigation operator &. constantly because nil responds to it gracefully. Your code doesn't crash when a database query returns no results—it just moves forward. Compare this to languages where you'd write explicit null checks at every step. In a ride-hailing application processing thousands of concurrent requests, this safety matters. A driver lookup might return nil during peak hours when the cache misses, and you need the code to degrade gracefully rather than raise an exception.
Strings are objects with methods too:
email = " USER@EXAMPLE.COM "
cleaned = email.strip.downcase # String methods chained together
slug = "Ruby Basics".parameterize # Rails extension on String
uuid = SecureRandom.uuid # Returns a String object with all string methodsIn real-world Rails apps, this means you can trust that "hello".reverse works, or that you can call .include? on an array without checking its type first. The object model is consistent.
Blocks with do...end and { }
Blocks are among the most powerful features in Ruby, and they're everywhere in Rails. A block is a chunk of code you pass to a method. It's not a lambda or a proc (though those exist too)—it's inline, it's readable, and it's how Rails idioms work.
You've written this a thousand times:
@posts = Post.where(published: true).map { |post| post.title }That { |post| post.title } is a block. The curly brace syntax is common for single-line operations. For multi-line blocks, we use do...end:
Post.where(published: true).each do |post|
puts "Processing: #{post.title}"
post.update(last_checked_at: Time.current)
NotificationService.new.notify(post.author)
endIn production, we see blocks used for iterating through database results, transforming collections, and handling callbacks. A background job in Sidekiq might look like:
class OrderProcessorJob
include Sidekiq::Job
def perform(order_ids)
Order.where(id: order_ids).find_each do |order|
order.items.each do |item|
InventoryService.reserve(item)
end
order.update(status: :processing)
end
end
endNotice we use find_each, which accepts a block and handles pagination automatically. This pattern prevents loading thousands of records into memory at once—a real concern when processing millions of orders daily. Without understanding blocks, you might write this incorrectly as Order.where(id: order_ids).each, which loads everything into memory and crashes in production when processing large datasets.
The key distinction: curly braces have higher precedence and work better for single operations, while do...end is more readable for complex logic and should be your default for anything over a few lines. This matters in debugging. A colleague reading your code should instantly recognize whether a block is a transformation or a side effect.
Blocks also power Rails conventions like validations and callbacks:
class Product < ApplicationRecord
validates :price, presence: true
validate do
if price.present? && price < 0
errors.add(:price, "cannot be negative")
end
end
before_save do
self.slug = name.parameterize if name.present?
end
endHere, blocks define behavior that runs at specific lifecycle points. Rails passes context into these blocks (like the model instance itself), and your code responds. In a trading system where prices update constantly, this pattern lets you validate complex invariants without cluttering your model with separate methods.
A common mistake is misunderstanding block scope. Variables defined outside a block are accessible inside it, but variables created inside the block don't leak out:
results = []
[1, 2, 3].each do |num|
squared = num * num # squared is local to this block
results << squared
end
puts squared # Error! squared is not defined hereIn production code, this has trapped many developers when using blocks with ActiveRecord. The data exists only within the block's scope.
Symbols like :name
Symbols are lightweight, immutable strings. You create them with a colon: :name, :user_id, :active. They're interned in memory, meaning :status always refers to the same object, regardless of where it appears in your code.
This makes them perfect for hash keys, which is why Rails leans on them:
def greet(name, greeting: "Hello")
"#{greeting}, #{name}!"
end
# Usage
greet("Alice", greeting: "Hi") # Returns "Hi, Alice!"Here, greeting: is a named parameter using a symbol internally. Rails ActiveRecord queries use symbols too:
User.where(status: :active).order(created_at: :desc)
User.pluck(:id, :email) # Column names as symbolsIn production e-commerce systems, we often build query builders and filters like this:
class OrderFilter
ALLOWED_STATUSES = [:pending, :processing, :shipped, :delivered].freeze
def initialize(params)
@status = params[:status]&.to_sym
@customer_id = params[:customer_id]
@date_range = params[:date_range]
end
def apply
query = Order.all
query = query.where(status: @status) if @status.in?(ALLOWED_STATUSES)
query = query.where(customer_id: @customer_id) if @customer_id.present?
query
end
endNotice we convert string params to symbols after validation. Symbols are more memory-efficient than strings for this use case because they're cached. If you're processing thousands of requests per second with millions of status lookups, this adds up. We've seen applications waste gigabytes of memory by using string keys instead of symbols in high-throughput scenarios.
However, there's a gotcha: never convert untrusted user input directly to symbols. Symbols are never garbage collected, so if an attacker sends infinite unique symbols, you'll leak memory. Always validate or convert explicitly like we show above.
Implicit returns
Ruby methods return the value of the last expression automatically. You don't need an explicit return statement (though you can use one for early exits).
This leads to clean, readable code:
def calculate_total(items)
items.sum(&:price)
end
def active_users
User.where(status: :active)
end
def generate_report
data = Report.last_7_days
formatted = data.map { |row| format_row(row) }
formatted
endThe last line of each method is what gets returned. This is powerful because it encourages shorter methods—when the return value is the focus, you naturally write functions that do one thing well. In a SaaS application with strict performance requirements, this discipline means fewer methods doing multiple things and easier optimization.
In Rails controllers, implicit returns simplify rendering logic:
class InvoicesController < ApplicationController
def show
@invoice = Invoice.find(params[:id])
@line_items = @invoice.line_items.eager_load(:product)
end
endThe controller method doesn't explicitly return anything, but Rails knows to render the view because that's the implicit return. Similarly, in ActiveRecord callbacks:
class Order < ApplicationRecord
before_save :calculate_totals
private
def calculate_totals
self.subtotal = items.sum(&:price)
self.tax = subtotal * TAX_RATE
self.total = subtotal + tax
end
endIf the last line were false or nil, it would halt the save, but returning the calculation values allows the callback to proceed. Understanding implicit returns helps you debug why a method isn't behaving as expected. We've debugged production issues where a callback silently failed to save because the developer inadvertently made the last line return nil.
Early returns are still idiomatic when you need to exit immediately:
def process_payment(order)
return false unless order.valid?
return false if order.total.zero?
# Continue with payment logic
PaymentGateway.charge(order)
endThis pattern is clear: fail fast, with explicit return statements signaling that method execution stops here. Your teammates will understand that subsequent code doesn't run if these conditions fail.
Truthiness and falsy values
In Ruby, only false and nil are falsy. Everything else is truthy, including 0 and empty strings. This differs from Python or JavaScript:
if 0
puts "This prints! 0 is truthy in Ruby"
end
if ""
puts "This prints! Empty string is truthy in Ruby"
end
if []
puts "This prints! Empty array is truthy in Ruby"
endIn production Rails code, this catches people constantly. When checking if something exists, be explicit:
# Risky - fails if count is 0
if order.items.count
# Process items
end
# Better
if order.items.any?
# Process items
end
# Also fine
if order.items.present? # Rails method
# Process items
endThe .present? Rails method returns true if an object is not blank. It's your safest bet in Rails applications. Similarly, .blank? returns true for nil, false, empty strings, empty arrays, and empty hashes. These methods prevent subtle bugs in payment processing or inventory systems where falsy behavior matters.
Common mistakes
We've seen teams stumble on these patterns in production:
- Treating symbols as strings:
params[:user_id]andparams["user_id"]are different. Rails typically converts form params to strings, so be careful with your key lookups. Useparams.permit(:user_id)to handle this safely. A real bug we debugged: querying User.where(id: @params[:user_id]) where @params came from JSON, returning no results because the key was a string. - Mixing block styles inconsistently: A codebase with random switches between
{ }anddo...endis harder to review. Pick a standard: one-liners use braces, multi-line use do...end. - Forgetting implicit returns cause issues: If your method has complex logic with multiple branches, add explicit
returnstatements for clarity. Future readers will thank you. We've seen data corruption in trading systems where an implicit nil return halted a critical save. - Assuming everything being an object means you can call any method: Not every object responds to every method. Always check the documentation or test in a console before assuming a method exists on an object type. Call
.methodson an object to see what's available. - Converting untrusted input to symbols: Never do
params[:action].to_symwithout validation. Use a whitelist approach as shown in the OrderFilter example. - Misunderstanding block scope: Variables created inside blocks don't leak outside. This is by design, but it surprises developers from languages with function scope instead of block scope.
Key takeaways
- Every value in Ruby is an object with methods—this means you can chain operations and call built-in functionality instead of writing utility functions. Safe navigation with nil prevents crashes in production.
- Blocks are fundamental to Rails idioms like
.map,.each, andfind_each. Use curly braces for one-liners and do...end for multi-line logic. They're not just syntax sugar; they're how Rails queries scale efficiently. - Symbols are lightweight, interned strings perfect for hash keys and parameters. They're more memory-efficient than strings in high-frequency scenarios. Protect against symbol DoS by validating user input.
- Implicit returns make Ruby methods cleaner and encourage single-responsibility functions. Use explicit
returnfor early exits and complex logic. Understand thatfalseandnilare the only falsy values. - Understanding these basics prevents bugs in production: safe navigation with nil objects, efficient filtering with symbols, database queries that don't load entire tables into memory, and predictable method behavior with implicit returns.






Comments
Loading comments…