What Rails Actually Wants: Tidying Controllers and Views Without Service Object Explosion

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:

  1. Load a resource
  2. Check permissions
  3. Reject if unauthorized

Example:

project = Project.find(params[:project_id])
unless project.members.exists?(current_user.id)
raise Unauthorized
end
issue = Issue.find(params[:id])

This leaks domain knowledge into imperative checks and risks security bugs.

Article content

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

Article content

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: :person
end

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:

  1. Extract request input
  2. Invoke domain logic (models or façade objects)
  3. Pass results to the view
  4. 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.



Article content

Leave a comment