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
Converted and distributed by TomeVault — claim your Tome and manage your conversions.