
February 20, 2026
Lessons from RailsTokyo 2026 on using ActiveRecord as a relational engine—not just an ORM
Modern Rails teams often inherit a paradox: controllers must be “thin,” views must be “dumb,” models must be “fat,” and yet production apps accumulate service objects, query objects, presenters, decorators, serializers, policies, and helpers until the architecture resembles a dependency matryoshka.
At RailsTokyo #3 (2026), Kyosuke Morohashi presented a deceptively simple alternative: stop fighting Rails — start following its intent. The talk argues that much accidental complexity comes not from large codebases, but from ignoring the design assumptions embedded in Rails itself.
👉 Source slides: tidying-rails-controllers-and-v…
This article distills the most technically consequential ideas for senior Rails engineers and architecture-minded teams.
Rails Is a Relational System First
ActiveRecord is often treated as a persistence layer with business logic on top. In reality, Rails ships a full relational algebra expressed as Ruby objects.
ActiveRecord::Relation supports:
- Selection (where)
- Projection (select)
- Join (joins, includes)
- Composition (merge, scopes)
- Lazy evaluation
The presentation emphasizes that Relation objects are the primary abstraction—not arrays of models.
Queries are executed only when needed; until then you are composing relational intent.
This enables powerful architectural moves: logic can migrate between model, controller, and view without changing behavior, because nothing has executed yet.
Authorization Should Be Relational, Not Imperative
A common anti-pattern in Rails apps:
- Load a resource
- Check permissions
- Reject if unauthorized
Example:
project = Project.find(params[:project_id])unless project.members.exists?(current_user.id) raise Unauthorizedendissue = Issue.find(params[:id])
This leaks domain knowledge into imperative checks and risks security bugs.

The Rails-native approach: search within accessible sets
From the presentation’s Issue-tracker example:
@project = current_person.projects.find_by!(name: params[:project_name])@issue = @project.issues.find(params[:id])
No manual checks. If access is invalid:
➡️ ActiveRecord::RecordNotFound
This is not just cleaner — it is safer and more performant. Authorization becomes a property of the data model.
The slide explicitly notes that controller complexity often comes from access control logic, which associations can eliminate. tidying-rails-controllers-and-v…
Model the Domain Correctly: has_many :through Everywhere It Makes Sense

Rails evolved from HABTM to increasingly expressive “through” associations for a reason: real domains are relational graphs.
Consider assigning people to issues within projects. A naïve model:
Issue belongs_to :person
But what if the person leaves the project? Should assignments survive? Should they disappear?
The talk proposes a richer model:
- Assignment belongs to Membership (person-in-project)
- Membership belongs to Person and Project
- Issue accesses assignees through memberships
class Issue < ApplicationRecord has_many :assignments has_many :memberships, through: :assignments has_many :assignees, through: :memberships, source: :personend
This allows:
✔ Correct lifecycle behavior ✔ No orphaned data ✔ Efficient joins ✔ Natural domain semantics
ActiveRecord generates the necessary SQL automatically.
The slides highlight even “through of through” associations as both valid and powerful. tidying-rails-controllers-and-v…
Controllers Are Interfaces, Not Data Warehouses
In Rails, instance variables are simply the bridge between controller and view.
They are not object state in the classical OOP sense.
Yet controllers frequently accumulate unrelated data:
def index @issues = project.issues @recent_news = project.news.limit(3) @sidebar_links = ... @stats = ...end
The presentation argues that data unrelated to the action’s core responsibility should not be loaded here.
Controllers should provide only what all representations require.
Why?
Because Rails actions can render multiple formats:
- HTML
- JSON
- Turbo Stream
- Markdown (increasingly)
- API responses
Loading presentation-specific data at the controller level couples unrelated outputs.
Yes, Loading Data in Views Can Be Correct
Rails dogma often says:
“Never access the database from views.”
The talk clarifies an important nuance:
❌ Writing raw SQL in templates is bad ✔ Using ActiveRecord relations is fine
Example partial:
<% project.news.published .order(created_at: :desc) .limit(3) .each do |news| %> <li><%= link_to news.title, project_news_path(project, news) %></li><% end %>
Why this works:
- It expresses intent using domain language
- It composes existing scopes
- It executes lazily only when rendered
- It avoids controller bloat
ActiveRecord::Relation is designed for exactly this flexibility. tidying-rails-controllers-and-v…
Representation-Specific Includes Belong Near the Representation
Another subtle but impactful idea:
Avoid preloading associations in controllers when they are needed only by specific views.
Typical pattern:
@issues = project.issues.includes(creator: :avatar)
But what if:
- HTML needs avatars
- JSON does not
- Turbo Stream renders only titles
Better:
<% @issues.includes(creator: :avatar).each do |issue| %> <%= image_tag issue.creator.avatar.url %> <%= issue.title %><% end %>
Because Relations are lazy, the include happens only when that template is rendered.
The slides note that controller-level includes often linger after views change, becoming hidden performance problems later. tidying-rails-controllers-and-v…
Partials as Lightweight Components
Without introducing new frameworks or gems, partials can act as components with explicit inputs:
<%= render "projects/news", project: @project %>
Inside:
<%# locals: (project:) %>...
Benefits:
- Encapsulation of presentation logic
- Reduced global instance variables
- Easy caching
- Minimal indirection
The presentation positions this as a pragmatic alternative to introducing heavier component systems prematurely.
What Remains as the Controller’s True Responsibility?
After “tidying,” controllers shrink to orchestration:
- Extract request input
- Invoke domain logic (models or façade objects)
- Pass results to the view
- Decide render or redirect
No architectural overhaul required.
Just better use of what Rails already provides.
Why This Matters in 2026
Many Rails codebases now resemble Java enterprise applications:
- Service layers everywhere
- Authorization scattered across objects
- Excessive indirection
- Poor locality of behavior
This approach offers a counter-movement:
👉 Lean into relational modeling and lazy composition instead of defensive layering.
It does not reject service objects, policies, or components. It simply treats them as optional tools rather than mandatory architecture.
The Core Insight
Rails is opinionated not only about conventions but about where complexity should live:
- Domain relationships → models and associations
- Access control → relational scope
- Presentation concerns → views
- Orchestration → controllers
When those boundaries align, code shrinks naturally.
Final Thoughts
The most striking aspect of the RailsTokyo presentation is not novelty but restraint.
No new gems. No paradigm shifts. No architectural manifestos.
Just a reminder that Rails already contains a sophisticated design language—one that many teams have drifted away from.
Sometimes the fastest way forward is not adding layers, but removing them.
