DHH's Rails Coding Style
Vanilla Rails is enough โ The 37signals philosophy
DHH (David Heinemeier Hansson) is the creator of Rails and CTO of 37signals (Basecamp, HEY). His coding style, refined over decades of writing production Rails code, is based on "simplicity" and "trusting the framework."
1. Vanilla Rails Is Enough
The principle is to minimize habitually installed external libraries (Devise, Redis, Sidekiq, etc.) and maximize the use of built-in framework features.
In practice, 37signals reduces external dependencies and handles background jobs, caching, etc. using only the database, simplifying deployment and operations.
# โ Habitually added external gems
gem 'devise' # โ has_secure_password is often enough
gem 'sidekiq' # โ Replaceable with Rails 8's Solid Queue
gem 'redis' # โ DB-based alternatives: Solid Cache, Solid Cable
gem 'pundit' # โ Simple before_action callbacks may suffice
# โ
Review Rails built-ins first
has_secure_password # Authentication
ActiveJob + Solid Queue # Background jobs
Rails.cache + Solid Cache # Caching
Action Cable + Solid Cable # WebSocket
Key point: Before adding a gem, ask yourself: "Can this be solved with Rails built-in features?"
2. Map Everything to CRUD
Instead of creating custom actions, create new resources and map them to standard CRUD (Create/Read/Update/Delete).
# โ Custom actions โ controller bloat
resources :cards do
member do
patch :close # CardsController#close
patch :reopen # CardsController#reopen
patch :archive # CardsController#archive
end
end
# โ
New resources for CRUD mapping โ consistent pattern
resources :cards do
resource :closure, only: [:create, :destroy] # create=close, destroy=reopen
resource :archival, only: [:create, :destroy] # create=archive, destroy=restore
end
This way:
ClosuresController#create= close cardClosuresController#destroy= reopen cardAll controllers use only the standard 7 actions (index/show/new/create/edit/update/destroy)
Routing is RESTful and predictable
3. Manage State as Records, Not Booleans
Instead of adding true/false columns for specific states, create separate records to manage them.
# โ Boolean column โ no history
class Card < ApplicationRecord
# closed: boolean (column)
# When was it closed? Who closed it? No way to know
end
# โ
Managed as records โ natural history
class Closure < ApplicationRecord
belongs_to :card
belongs_to :user # Who closed it
# created_at # When it was closed (automatic)
end
class Card < ApplicationRecord
has_many :closures
def closed?
closures.exists?
end
def closed_by
closures.last&.user
end
end
Benefits:
Naturally records when and who closed it
Full close/open history queryable
Automatic audit log for state changes
State cancellation via destroy (clearer than boolean toggle)
Learning Method
Browse 37signals' open source code directly on GitHub:
Campfire โ 37signals' chat app (built with only Rails built-in features)
ONCE โ Buy once, use forever apps (minimal external dependencies philosophy)
Basecamp โ DHH's core product
Practical exercise: In your next project, remove one external library you habitually use and implement it yourself. Start by building auth without Devise using has_secure_password, or replacing Sidekiq with Solid Queue.
Key Points
Before adding features โ ask "Can Rails built-ins handle this?" first
When custom actions are needed โ extract to new resource (Controller) with CRUD mapping
State change features โ create separate model (record) instead of boolean columns
Before adding gems to Gemfile โ review Rails 8 Solid series (Queue/Cache/Cable)
Learn patterns by reading 37signals open source (Campfire, ONCE) code
In your next project, remove one external gem and implement it yourself
Pros
- ✓ Simpler deployment/ops (no Redis, no separate workers)
- ✓ Fewer dependencies means fewer security vulnerabilities
- ✓ Controllers maintain consistent 7-action pattern
- ✓ State change history is automatically recorded
- ✓ Predictable API with RESTful routing
- ✓ Minimal breakage on Rails upgrades
Cons
- ✗ Initially slower to implement yourself (learning cost)
- ✗ Over-splitting resources increases controller file count
- ✗ May confuse teammates used to conventional Rails (Devise, etc.)
- ✗ Large-scale services may still need Redis/Sidekiq for performance