Code Style Guide
Comprehensive guide for Rails code style and conventions.
Note: This guide represents opinionated coding style based on Basecamp/37signals practices. While strongly aligned with Rails philosophy and conventions, some preferences (such as favoring expanded conditionals over guard clauses, and the
_later/_nownaming pattern) differ from broader Rails community norms. These are battle-tested patterns from production codebases, not universal standards. Adapt these guidelines to match your team's preferences and project needs.
Philosophy
Write code that is a pleasure to read. Code style matters because:
- •Readability - Code is read more often than written
- •Consistency - Predictable patterns reduce cognitive load
- •Maintainability - Clear code is easier to modify
- •Collaboration - Shared conventions improve team velocity
Conditional Returns
Prefer Expanded Conditionals
Prefer expanded if/else over guard clauses:
# ❌ Bad - Guard clause
def todos_for_new_group
ids = params.require(:todolist)[:todo_ids]
return [] unless ids
@bucket.recordings.todos.find(ids.split(","))
end
# ✅ Good - Expanded conditional
def todos_for_new_group
if ids = params.require(:todolist)[:todo_ids]
@bucket.recordings.todos.find(ids.split(","))
else
[]
end
end
Why: Guard clauses can be hard to read, especially when nested.
Exception: Early Returns
Guard clauses are acceptable when:
- •The return is right at the beginning of the method
- •The main method body is non-trivial (several lines)
# ✅ Acceptable - Early return for clarity
def after_recorded_as_commit(recording)
return if recording.parent.was_created?
if recording.was_created?
broadcast_new_column(recording)
else
broadcast_column_change(recording)
end
end
Methods Ordering
Class-Level Organization
Order methods in classes:
- •Class methods
- •Public methods (with
initializefirst if present) - •Private methods
class Card < ApplicationRecord
# 1. Class methods
def self.find_stale
where(last_active_at: ..1.month.ago)
end
# 2. Public methods
def initialize(*args)
super
@initialized = true
end
def close
create_closure!
end
def reopen
closure.destroy
end
# 3. Private methods
private
def validate_state
# ...
end
end
Invocation Order (Vertical Reading)
Order methods based on their invocation order. This helps understand code flow:
class SomeClass
def process
step_one
step_two
end
private
# Step one and its sub-methods
def step_one
step_one_a
step_one_b
end
def step_one_a
# Implementation
end
def step_one_b
# Implementation
end
# Step two and its sub-methods
def step_two
step_two_a
step_two_b
end
def step_two_a
# Implementation
end
def step_two_b
# Implementation
end
end
Why: Reading top-to-bottom mirrors execution flow.
Bang Methods (!)
Only Use ! When Non-Bang Version Exists
Rule: Only use ! for methods that have a corresponding version without !.
# ✅ Good - Has save counterpart def save! raise unless save end # ❌ Bad - No close counterpart def close! create_closure! end # ✅ Good - Just use close def close create_closure! end
Why: We don't use ! to flag destructive actions. Many destructive methods in Ruby and Rails don't end with ! (destroy, delete, update, etc.).
Examples of correct usage:
- •
save/save! - •
create/create! - •
update/update! - •
destroy/destroy!
Visibility Modifiers
No Newline Under Modifier, Indent Content
# ✅ Good
class SomeClass
def public_method
# Implementation
end
private
def private_method_1
# Implementation
end
def private_method_2
# Implementation
end
end
# ❌ Bad - Extra newline, no indentation class SomeClass def public_method end private def private_method end end
Module with Only Private Methods
If a module has only private methods, mark private at the top with an extra newline but don't indent:
# ✅ Good - Module with only private methods
module SomeModule
private
def some_private_method
# Implementation
end
def another_private_method
# Implementation
end
end
CRUD Controllers
Model Actions as Resources
Model web endpoints as CRUD operations on resources (REST). When an action doesn't map to a standard CRUD verb, introduce a new resource:
# ❌ Bad - Custom actions resources :cards do post :close post :reopen post :gild end # ✅ Good - Actions as resources resources :cards do resource :closure # POST = close, DELETE = reopen resource :goldness # POST = gild, DELETE = ungild end
Controller and Model Interactions
Vanilla Rails Approach
Favor thin controllers directly invoking a rich domain model. Don't use services or other artifacts to connect the two unless necessary.
Simple Operations
Invoking plain Active Record operations is totally fine:
# ✅ Good - Direct Active Record
class Cards::CommentsController < ApplicationController
def create
@comment = @card.comments.create!(comment_params)
end
end
Complex Operations
For complex behavior, prefer clear, intention-revealing model APIs:
# ✅ Good - Intention-revealing model method
class Cards::GoldnessesController < ApplicationController
def create
@card.gild
end
end
# In model
class Card < ApplicationRecord
def gild
transaction do
create_goldness!
track_event :gilded
end
end
end
Services (When Justified)
When justified, it's fine to use services or form objects, but don't treat them as special artifacts:
# ✅ Acceptable when complexity warrants it
class SignupsController < ApplicationController
def create
Signup.new(email_address: params[:email]).create_identity
end
end
Async Operations in Jobs
Shallow Jobs That Delegate
Write shallow job classes that delegate logic to domain models:
Naming convention:
- •Use
_latersuffix for methods that enqueue a job - •Use
_nowsuffix for the synchronous version
# Model concern
module Event::Relaying
extend ActiveSupport::Concern
included do
after_create_commit :relay_later
end
def relay_later
Event::RelayJob.perform_later(self)
end
def relay_now
webhooks.active.each { |webhook| webhook.trigger(self) }
end
end
# Job delegates to model
class Event::RelayJob < ApplicationJob
def perform(event)
event.relay_now
end
end
Why: Keeps jobs thin and testable. Business logic stays in models.
General Code Style
Whitespace
# ✅ Good - One space around operators
x = 1 + 2
hash = { key: value }
array = [1, 2, 3]
# ❌ Bad
x=1+2
hash={key:value}
Line Length
- •Keep lines under 120 characters when possible
- •Break long lines logically
# ✅ Good - Broken at logical points User.where(active: true) .where(verified: true) .order(created_at: :desc) # ❌ Bad - One long line User.where(active: true).where(verified: true).order(created_at: :desc).limit(10).offset(20)
String Literals
# ✅ Prefer double quotes for strings message = "Hello, world!" # ✅ Single quotes for strings that don't need interpolation or escaping sql = 'SELECT * FROM users WHERE id = ?'
Hash Syntax
# ✅ Good - New hash syntax for symbol keys
user = { name: "Alice", email: "alice@example.com" }
# ✅ Old syntax when keys are not symbols
config = { "Content-Type" => "application/json" }
Code Organization Principles
1. Extract Complex Conditionals
# ❌ Bad - Complex inline conditional if user.admin? && (user.verified? || user.trusted?) && user.active? grant_access end # ✅ Good - Extracted to method def can_access? user.admin? && (user.verified? || user.trusted?) && user.active? end if can_access? grant_access end # ✅ Even better - Method on user if user.can_access? grant_access end
2. Single Responsibility
Each method should have one clear purpose:
# ❌ Bad - Multiple responsibilities
def process_user
user.verify_email
user.send_welcome_email
user.subscribe_to_newsletter
user.create_default_settings
end
# ✅ Good - Separate concerns
def process_user
verify_user
welcome_user
setup_user_account
end
private
def verify_user
user.verify_email
end
def welcome_user
user.send_welcome_email
end
def setup_user_account
user.subscribe_to_newsletter
user.create_default_settings
end
3. Intention-Revealing Names
Use clear, descriptive names:
# ❌ Bad - Unclear names def process data = fetch x = transform(data) save(x) end # ✅ Good - Clear names def import_users csv_data = fetch_csv_from_api user_records = parse_csv_to_users(csv_data) save_users_to_database(user_records) end
Best Practices Summary
✅ DO
- •Expanded conditionals over guard clauses (except early returns)
- •Order methods by invocation flow
- •Indent under visibility modifiers (no extra newline)
- •Only use ! when non-bang version exists
- •Model actions as resources in routes
- •Thin controllers that delegate to models
- •Shallow jobs with
_later/_nowpattern - •Intention-revealing names for methods and variables
- •Extract complex conditionals to methods
- •One responsibility per method
❌ DON'T
- •Guard clauses everywhere - Use expanded conditionals
- •Random method order - Follow invocation order
- •Extra newlines after private - Keep it clean
- •Bang methods without counterparts - Just use regular names
- •Custom controller actions - Use resources
- •Fat controllers - Delegate to models
- •Business logic in jobs - Keep jobs thin
- •Cryptic variable names - Be descriptive
- •Complex inline conditionals - Extract to methods
- •Multiple responsibilities - Keep focused
Summary
- •Conditionals: Expanded over guards (except early returns)
- •Methods: Ordered by invocation, clear names
- •Visibility: Indent under modifiers, no extra newlines
- •Bang: Only when non-bang version exists
- •Controllers: Thin, RESTful, resource-oriented
- •Jobs: Shallow, delegate to models, use
_later/_now - •Organization: Single responsibility, intention-revealing names