The Hidden DSL Inside Every Rails Model

The Hidden DSL Inside Every Rails Model
The Hidden DSL Inside Every Rails Model

June 10, 2026

Most Rails developers use belongs_to, has_many, scope, and validates every day.

We type them almost without thinking.

class User < ApplicationRecord
belongs_to :company
validates :email, presence: true
scope :active, -> { where(active: true) }
end

But here’s something interesting:

None of those are Ruby keywords.

They’re methods.

In fact, a Rails model is less like a traditional Ruby class and more like a Domain Specific Language (DSL) built on top of Ruby.

Once you see it, it’s hard to unsee.

Tokyo Topographic Map
Built for Ruby on Rails

Build Maps Without
Google APIs

Generate beautiful production-ready maps directly from your Rails backend. Fast rendering, zero external dependencies, full control.

✓ No API fees ✓ Self-hosted ✓ Rails Native ✓ Fast Rendering
Why developers switch
Replace expensive map stacks.

Stop relying on third-party map billing and bloated JS libraries. Render static or dynamic maps directly in Ruby.

Try It Now
Tokyo MapView Demo

The Empty Model

A Rails model starts life as a completely ordinary Ruby class.

class User < ApplicationRecord
end

At first glance, there’s nothing special here.

User inherits from ApplicationRecord, which inherits from ActiveRecord::Base.

That’s where the magic begins.

By inheriting from Active Record, the class gains hundreds of methods that allow us to describe data, relationships, validations, callbacks, and behavior using a concise DSL.

Associations

Consider this:

class User < ApplicationRecord
belongs_to :company
has_many :posts
end

When Rails reads these lines, it isn’t creating relationships in the database.

It’s executing methods.

Those methods generate additional behavior behind the scenes.

After declaring belongs_to :company, Rails creates methods such as:

user.company
user.company = company
user.build_company
user.create_company

A single line of DSL expands into a collection of Ruby methods.

Validations

The same pattern appears with validations.

class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
end

Again, validates is just a method.

Under the hood Rails builds validator objects and registers them with the model.

Later, when save runs, those validators participate in the model lifecycle.

What looks declarative is actually executable Ruby code configuring the class.

Scopes

Scopes are another example.

scope :active, -> { where(active: true) }
scope :recent, -> { order(created_at: :desc) }

The scope method dynamically defines additional class methods.

Afterward, we can write:

User.active.recent

This fluent query interface feels almost like a language designed specifically for expressing business rules.

That’s because it is.

Enums

Enums provide one of the clearest examples of Rails generating behavior.

enum :status, {
pending: 0,
active: 1,
suspended: 2
}

This single declaration creates methods such as:

user.active?
user.pending?
user.suspended?
user.active!

and scopes like:

User.active
User.pending
User.suspended

One line becomes an entire API.

Callbacks

Callbacks often look like configuration:

before_create :generate_token
after_commit :send_welcome_email

But they are also method calls.

Rails records these declarations and later injects them into the model lifecycle.

The model is effectively building a pipeline of behavior during class definition.

Delegates

Even delegation participates in the DSL.

delegate :name, to: :company

Rails dynamically creates:

user.name

which forwards the call to:

user.company.name

Again, a tiny declaration generates real Ruby methods.

Beyond the Usual Suspects

Most developers think about associations, validations, and scopes, but Rails models can host many more DSL elements:

attribute :admin, :boolean, default: false
encrypts :ssn
has_one_attached :avatar
has_rich_text :content
accepts_nested_attributes_for :profile
store_accessor :preferences, :theme, :language
serialize :settings, JSON

Each one teaches the model new capabilities through a simple declaration.

That’s the essence of a DSL.

A Typical Rails Model

After years of development, many models start looking like this:

class User < ApplicationRecord
belongs_to :company
has_many :posts
enum :status, {
pending: 0,
active: 1,
suspended: 2
}
validates :email, presence: true
scope :active, -> { where(status: :active) }
before_create :generate_token
delegate :company_name, to: :company, prefix: true
has_one_attached :avatar
def full_name
"#{first_name} #{last_name}"
end
private
def generate_token
self.token ||= SecureRandom.uuid
end
end

Every section represents another piece of the Active Record DSL.

  • Associations
  • Validations
  • Scopes
  • Enums
  • Callbacks
  • Delegates
  • Attributes
  • Attachments
  • Rich Text
  • Encryption
  • Methods

All living together inside a class that reads almost like a miniature language.

Why This Matters

Understanding that Rails models are DSLs changes how you read code.

Instead of seeing a list of declarations, you start recognizing a series of method calls that configure a class at load time.

That perspective helps explain:

  • Why inheritance matters so much in Rails.
  • How gems add new model macros.
  • Why method lookup becomes important.
  • How Rails generates so much functionality from so little code.
  • Why Rails code often feels more expressive than plain Ruby.

The next time you write:

belongs_to :company

remember that you’re not using a keyword.

You’re invoking a method that teaches your model new behavior.

And that’s one of the most elegant ideas in all of Rails.


Suggested subtitle:

Most Rails developers use belongs_to, scope, and enum every day. Fewer realize they’re participating in one of the most powerful DSLs in the Ruby ecosystem.

Article content

Leave a comment