Ruby Development
Apply these patterns when working with Ruby code.
Naming Conventions
| Element |
Convention |
Example |
| Classes, Modules |
PascalCase |
UserService |
| Methods, variables |
snake_case |
find_user_by_id |
| Constants |
SCREAMING_SNAKE |
MAX_RETRIES |
| Predicates |
snake_case? |
active? |
| Dangerous methods |
snake_case! |
save! |
| Files |
snake_case |
user_service.rb |
Project Structure
lib/
├── my_app.rb
└── my_app/
├── version.rb
└── services/
spec/
├── spec_helper.rb
└── services/
Gemfile
Idioms
Blocks and Lambdas
users.map { |u| u.name }
users.map(&:name) # Symbol to proc
double = ->(n) { n * 2 }
double.call(5)
Enumerable
users.select(&:active?)
users.reject(&:banned?)
users.find { |u| u.id == 5 }
users.group_by(&:country)
orders.sum(&:total)
users.select(&:active?).sort_by(&:created_at).first(10).map(&:email)
Safe Navigation
user&.profile&.avatar_url
name = user.name || "Anonymous"
def expensive_calculation
@expensive_calculation ||= perform_calculation
end
Pattern Matching (Ruby 3+)
case response
in { status: 200, body: }
process(body)
in { status: 404 }
handle_not_found
end
Classes
class User
attr_reader :id, :email
attr_accessor :name
def initialize(id:, email:, name: nil)
@id = id
@email = email
@name = name
end
def active?
@status == :active
end
private
def save!
# Persist changes
end
end
Modules for Composition
module Timestampable
def touch
@updated_at = Time.current
end
end
class User
include Timestampable
end
Service Objects
class CreateUser
def initialize(user_repository:, email_service:)
@user_repository = user_repository
@email_service = email_service
end
def call(email:, name:)
user = @user_repository.create(email: email, name: name)
@email_service.send_welcome(user)
user
end
end
Error Handling
module MyApp
class Error < StandardError; end
class NotFoundError < Error; end
class ValidationError < Error
attr_reader :errors
def initialize(errors)
@errors = errors
super("Validation failed: #{errors.join(', ')}")
end
end
end
raise MyApp::NotFoundError, "User not found"
Result Objects
class Result
attr_reader :value, :error
def self.success(value) = new(value: value)
def self.failure(error) = new(error: error)
def success? = @error.nil?
def failure? = !success?
def then
return self if failure?
yield(value)
end
end
Testing with RSpec
RSpec.describe UserService do
subject(:service) { described_class.new(repository: repository) }
let(:repository) { instance_double(UserRepository) }
let(:user) { build(:user, id: 1, name: "John") }
describe "#find_by_id" do
context "when user exists" do
before { allow(repository).to receive(:find).with(1).and_return(user) }
it "returns the user" do
expect(service.find_by_id(1)).to eq(user)
end
end
end
end
Matchers
expect(result).to eq(expected)
expect(user).to be_nil
expect(list).to include(item)
expect { user.activate! }.to change(user, :status).to(:active)
expect { risky_op }.to raise_error(MyError)
Doubles
repository = instance_double(UserRepository)
allow(repository).to receive(:find).with(1).and_return(user)
mailer = instance_spy(UserMailer)
service.call
expect(mailer).to have_received(:send_welcome).with(user)
Requirements
- Use composition over inheritance
- Use keyword arguments for methods with 2+ parameters
- Return early with guard clauses
- Add
# frozen_string_literal: true to every file
- Use symbols for hash keys
- Use Enumerable methods over manual iteration
- Name predicate methods with
?
- Raise exceptions only for exceptional cases; use Result objects for expected failures