Small Things in Ruby That Make a Huge Difference
I've been writing Ruby for 15 years. And you know what Ive learned? The languages power isnt in the big features – its in the small, elegant details that compound over time.
These aren't flashy framework features or architectural patterns. These are the little Ruby idioms that, once you internalize them, make your code cleaner, shorter, and more expressive.
Let me show you what I mean.
Enumerable: Ruby's Secret Weapon
When I interview Ruby developers, I can immediately tell their experience level by how they use Enumerable methods. Juniors loop through everything. Seniors rarely write explicit loops.
The Progression of a Ruby Developer
Stage 1: The Beginner (Everything is a for loop)
# Find all published posts
published_posts = []
for post in posts
if post.published?
published_posts << post
end
end
# Calculate total price
total = 0
for item in cart_items
total = total + item.price
end
# Get user emails
emails = []
for user in users
emails << user.email
end
This works. But it's verbose, imperative, and un-Ruby-like.
Stage 2: The Intermediate (Discovers basic Enumerable)
# Find all published posts
published_posts = posts.select { |post| post.published? }
# Calculate total price
total = cart_items.sum { |item| item.price }
# Get user emails
emails = users.map { |user| user.email }
Much better. More declarative. More readable. Less noise.
Stage 3: The Advanced (Chains methods fluently)
# Get emails of active users with verified accounts
User.active
.verified
.order(:created_at)
.limit(100)
.pluck(:email)
# Group orders by status and calculate totals
orders.group_by(&:status)
.transform_values { |orders| orders.sum(&:total) }
# Find the most expensive item in each category
products.group_by(&:category)
.transform_values { |prods| prods.max_by(&:price) }
This is where Ruby shines. Readable, expressive, elegant.
Real-World Examples That Changed My Code
Let me show you some before/after transformations from actual projects.
Example 1: Finding duplicates
# Before (ugly nested loops)
duplicates = []
users.each_with_index do |user, i|
users.each_with_index do |other_user, j|
if i != j && user.email == other_user.email
duplicates << user unless duplicates.include?(user)
end
end
end
# After (one line)
duplicates = users.group_by(&:email)
.values
.select { |group| group.size > 1 }
.flatten
Example 2: Conditional transformations
# Before
result = []
items.each do |item|
if item.active?
result << item.name.upcase
end
end
# After
result = items.select(&:active?)
.map { |item| item.name.upcase }
# Even better (method chaining)
result = items.select(&:active?)
.map(&:name)
.map(&:upcase)
Example 3: Complex filtering
I had to implement a feature: "Find users who have placed orders in the last 30 days, spent over $100, and haven't left a review."
# Before (procedural nightmare)
eligible_users = []
users.each do |user|
recent_orders = []
user.orders.each do |order|
if order.created_at > 30.days.ago
recent_orders << order
end
end
total_spent = 0
recent_orders.each do |order|
total_spent += order.total
end
has_review = false
user.reviews.each do |review|
if review.created_at > 30.days.ago
has_review = true
break
end
end
if total_spent > 100 && !has_review
eligible_users << user
end
end
# After (declarative bliss)
eligible_users = users.select do |user|
recent_orders = user.orders.where('created_at > ?', 30.days.ago)
recent_orders.sum(:total) > 100 && user.reviews.recent.none?
end
But we can do even better with scopes:
# In User model
scope :with_recent_orders, -> {
joins(:orders).where('orders.created_at > ?', 30.days.ago).distinct
}
scope :high_spenders, -> (amount) {
joins(:orders)
.where('orders.created_at > ?', 30.days.ago)
.group('users.id')
.having('SUM(orders.total) > ?', amount)
}
scope :without_recent_reviews, -> {
where.not(id: Review.where('created_at > ?', 30.days.ago).select(:user_id))
}
# Usage
eligible_users = User.with_recent_orders
.high_spenders(100)
.without_recent_reviews
Clean. Testable. Reusable.
The Enumerable Methods You Should Master
Here are the ones I use constantly:
select / reject – Filtering
# Get active users
User.all.select(&:active?)
# Exclude admins
users.reject(&:admin?)
# Complex conditions
posts.select { |p| p.published? && p.comments_count > 10 }
map / pluck – Transformation
# Transform objects
users.map(&:email)
users.map { |u| [u.id, u.name] }.to_h
# From database (more efficient)
User.pluck(:email)
User.pluck(:id, :name) # Returns array of arrays
sum / count – Aggregation
# Sum orders.sum(&:total) Order.sum(:total) # Database-level, more efficient # Count users.count(&:active?) User.where(active: true).count # Database-level
group_by – Grouping
# Group users by role
users.group_by(&:role)
# => { "admin" => [...], "user" => [...] }
# Group orders by month
orders.group_by { |o| o.created_at.beginning_of_month }
# With transform_values
orders.group_by(&:status)
.transform_values { |orders| orders.sum(&:total) }
# => { "pending" => 1500, "completed" => 8500 }
find / detect – First match
# First user with this email
users.find { |u| u.email == 'john@example.com' }
# More efficient from database
User.find_by(email: 'john@example.com')
any? / all? / none? – Checks
# Any published posts?
posts.any?(&:published?)
# All users verified?
users.all?(&:verified?)
# No pending orders?
orders.none? { |o| o.status == 'pending' }
partition – Split into two groups
# Split users into active and inactive active, inactive = users.partition(&:active?)
zip – Combine arrays
names = ['Alice', 'Bob', 'Charlie']
ages = [25, 30, 35]
names.zip(ages)
# => [['Alice', 25], ['Bob', 30], ['Charlie', 35]]
names.zip(ages).to_h
# => { 'Alice' => 25, 'Bob' => 30, 'Charlie' => 35 }
reduce / inject – Fold
# Sum (though .sum is clearer)
[1, 2, 3, 4].reduce(0) { |sum, n| sum + n }
# Build a hash
users.reduce({}) { |hash, user|
hash.merge(user.id => user.name)
}
# More complex: Build a tree structure
comments.reduce({}) do |tree, comment|
tree[comment.parent_id] ||= []
tree[comment.parent_id] << comment
tree
end
The Power of Method Chaining
This is where Ruby becomes beautiful. You can chain Enumerable methods to build complex transformations:
# Get top 5 most active users who joined this year
User.where('created_at > ?', 1.year.ago)
.sort_by { |u| -u.posts.count }
.take(5)
.map { |u| { name: u.name, posts: u.posts.count } }
# Calculate average order value by customer tier
orders.group_by { |o| o.user.tier }
.transform_values { |orders| orders.sum(&:total) / orders.size }
.sort_by { |tier, avg| -avg }
.to_h
Read this out loud:
"Group orders by user tier, transform the values to average order value, sort by average descending, convert to hash."
That's exactly what the code does. That's the power of declarative programming.
Performance Considerations
A word of warning: Not all Enumerable methods are created equal.
# BAD: Loads all users into memory, then filters in Ruby
User.all.select { |u| u.active? }
# GOOD: Filters in database
User.where(active: true)
# BAD: N+1 queries
users.map { |u| u.posts.count }
# GOOD: Eager loading
User.includes(:posts).map { |u| u.posts.size }
# BAD: Multiple iterations
users.select(&:active?).count
users.select(&:active?).map(&:email)
# GOOD: One iteration, store result
active_users = users.select(&:active?)
active_users.count
active_users.map(&:email)
Rule of thumb:
- For ActiveRecord collections: Use database methods (
.where,.pluck,.sum) - For in-memory arrays: Use Enumerable methods (
.select,.map,.sum)
Refactoring: The Art of Making Code Better
Good Ruby code isn't written – it's refactored into existence.
Extract Methods Aggressively
Before:
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
@order.user = current_user
@order.total = @order.line_items.sum { |li| li.quantity * li.price }
if @order.total > 100
@order.discount = @order.total * 0.1
@order.total = @order.total - @order.discount
end
if current_user.premium?
@order.shipping_cost = 0
else
@order.shipping_cost = 10
end
@order.final_total = @order.total + @order.shipping_cost
if @order.save
OrderMailer.confirmation(@order).deliver_later
redirect_to @order, notice: 'Order created!'
else
render :new
end
end
end
After:
class OrdersController < ApplicationController
def create
@order = build_order
if @order.save
send_confirmation_email
redirect_to @order, notice: 'Order created!'
else
render :new
end
end
private
def build_order
Order.new(order_params).tap do |order|
order.user = current_user
order.calculate_totals
order.apply_discount_if_eligible
order.calculate_shipping
end
end
def send_confirmation_email
OrderMailer.confirmation(@order).deliver_later
end
end
# In Order model
class Order < ApplicationRecord
def calculate_totals
self.total = line_items.sum { |li| li.quantity * li.price }
end
def apply_discount_if_eligible
return unless total > 100
self.discount = total * 0.1
self.total -= discount
end
def calculate_shipping
self.shipping_cost = user.premium? ? 0 : 10
self.final_total = total + shipping_cost
end
end
Why this is better:
- Controller is thin (12 lines → 8 lines)
- Business logic is in the model (where it belongs)
- Each method does one thing
- Easy to test each piece independently
- Easy to read and understand
Use Ruby's Safe Navigation Operator
Before:
if user && user.profile && user.profile.avatar && user.profile.avatar.url avatar_url = user.profile.avatar.url else avatar_url = '/default-avatar.png' end
After:
avatar_url = user&.profile&.avatar&.url || '/default-avatar.png'
One line. Clear. Elegant.
Replace Conditionals with Polymorphism
Before:
class PaymentProcessor
def process(payment)
if payment.type == 'credit_card'
charge_credit_card(payment)
elsif payment.type == 'paypal'
charge_paypal(payment)
elsif payment.type == 'bank_transfer'
charge_bank_transfer(payment)
elsif payment.type == 'crypto'
charge_crypto(payment)
end
end
end
After:
class CreditCardPayment < Payment
def process
# Credit card specific logic
end
end
class PaypalPayment < Payment
def process
# PayPal specific logic
end
end
# Usage
payment.process # Calls the right method based on payment type
Benefits:
- No conditionals
- Each payment type is isolated
- Easy to add new payment methods (Open/Closed Principle)
- Easier to test
Use try or &. for Nil Safety
Before:
def user_email
if @user && @user.respond_to?(:email)
@user.email
else
'No email'
end
end
After:
def user_email @user&.email || 'No email' end
Extract Complex Conditions
Before:
if user.active? && user.verified? && user.orders.any? && !user.banned? && user.created_at < 1.year.ago # Do something end
After:
if user.eligible_for_promotion? # Do something end # In User model def eligible_for_promotion? active? && verified? && orders.any? && !banned? && veteran? end def veteran? created_at < 1.year.ago end
Name your conditions. It's self-documenting.
Use tap for Side Effects
Before:
user = User.new(user_params) user.generate_token user.set_default_role user.send_welcome_email user.save user
After:
User.new(user_params).tap do |user| user.generate_token user.set_default_role user.send_welcome_email user.save end
Or even better, use callbacks:
class User < ApplicationRecord after_create :generate_token after_create :set_default_role after_create :send_welcome_email end User.create(user_params) # Everything happens automatically
Code Readability: Write Code for Humans
Code is read 10x more than it's written. Optimize for readability.
Use Descriptive Names
Before:
def p(u, o)
t = o.sum(&:t)
if u.p?
t * 0.9
else
t
end
end
What does this even do?
After:
def calculate_total_price(user, orders)
subtotal = orders.sum(&:total)
if user.premium?
apply_discount(subtotal)
else
subtotal
end
end
def apply_discount(amount)
amount * 0.9
end
Now it's clear.
Avoid Magic Numbers
Before:
def shipping_cost(weight)
if weight < 5
10
elsif weight < 20
25
else
50
end
end
After:
ruby
LIGHT_PACKAGE_THRESHOLD = 5
MEDIUM_PACKAGE_THRESHOLD = 20
LIGHT_SHIPPING_COST = 10
MEDIUM_SHIPPING_COST = 25
HEAVY_SHIPPING_COST = 50
def shipping_cost(weight)
case weight
when 0...LIGHT_PACKAGE_THRESHOLD
LIGHT_SHIPPING_COST
when LIGHT_PACKAGE_THRESHOLD...MEDIUM_PACKAGE_THRESHOLD
MEDIUM_SHIPPING_COST
else
HEAVY_SHIPPING_COST
end
end
Every number has a name. Every name has meaning.
Use Guard Clauses
Before:
def process_refund(order)
if order.present?
if order.paid?
if order.refundable?
# Actually process refund
order.refund!
send_refund_email(order)
true
else
false
end
else
false
end
else
false
end
end
Nesting hell.
After:
def process_refund(order) return false if order.blank? return false unless order.paid? return false unless order.refundable? order.refund! send_refund_email(order) true end
Flat. Readable. Clear failure conditions up front.
Use Keyword Arguments for Clarity
Before:
create_user('John', 'john@example.com', 'password123', true, false, 'admin')
What do those booleans mean? What's 'admin'?
After:
create_user( name: 'John', email: 'john@example.com', password: 'password123', verified: true, banned: false, role: 'admin' )
Crystal clear.
Prefer Positive Conditionals
Before:
unless !user.inactive? # Do something end
Double negative. Brain hurts.
After:
if user.active? # Do something end
Reads like English.
Use Query Methods for Boolean Returns
Before:
def check_if_user_can_edit_post(user, post) user.id == post.author_id || user.admin == true end if check_if_user_can_edit_post(current_user, @post) # Allow editing end
After:
def can_edit?(user) authored_by?(user) || user.admin? end def authored_by?(user) author == user end # In controller if @post.can_edit?(current_user) # Allow editing end
Reads like a question. Returns a boolean. Perfect.
The Compound Effect
None of these things alone will revolutionize your code. But together, over thousands of lines, they compound.
Compare this codebase:
# Beginner code
def get_active_user_emails_who_spent_over_100
result = []
for user in User.all
if user.active == true
total = 0
for order in user.orders
total = total + order.total
end
if total > 100
result.push(user.email)
end
end
end
result
end
To this:
# Experienced Ruby developer
User.active
.with_orders_over(100)
.pluck(:email)
# With scope in User model
scope :with_orders_over, ->(amount) {
joins(:orders)
.group('users.id')
.having('SUM(orders.total) > ?', amount)
}
Same functionality. 10x less code. Infinitely more readable.
Final Thoughts
These "small things" are what separate okay Ruby code from great Ruby code:
- Master Enumerable – Stop writing loops, start composing transformations
- Refactor relentlessly – Extract methods, use polymorphism, eliminate duplication
- Write for humans – Clear names, guard clauses, positive conditionals
Ruby gives you the tools to write beautiful, expressive code. Use them.
Your future self (and your teammates) will thank you.