๐Ÿ’Ž

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 card

  • ClosuresController#destroy = reopen card

  • All 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

1

Before adding features โ†’ ask "Can Rails built-ins handle this?" first

2

When custom actions are needed โ†’ extract to new resource (Controller) with CRUD mapping

3

State change features โ†’ create separate model (record) instead of boolean columns

4

Before adding gems to Gemfile โ†’ review Rails 8 Solid series (Queue/Cache/Cable)

5

Learn patterns by reading 37signals open source (Campfire, ONCE) code

6

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

Use Cases

Auth: Devise โ†’ has_secure_password (Rails built-in) Background jobs: Sidekiq โ†’ Solid Queue (Rails 8) Caching: Redis โ†’ Solid Cache (DB-based) Close card: patch :close โ†’ ClosuresController#create Unsubscribe: patch :unsubscribe โ†’ CancellationsController#create Publish post: update(published: true) โ†’ PublicationsController#create Complete order: update(completed: true) โ†’ CompletionsController#create