The Missing Pieces

Once well defined roles are chosen for each layer of Rails, it becomes obvious that there is no answer about what category of objects should handle app-specific business logic. Controllers are responsible for HTTP, views manage formatting and presentation, and records deal with persistence. There is a big gap between these layers where the actual core piece of the app should live, agnostic of communication or databases.

Diagram 2

The cloud with questions marks above is exactly where the hard work of designing a good Rails architecture sits. That is the brain of the app, beyond simply transporting data from the database to the web via HTTP and markups. That is where the rules for permissions, validations, user flows, collaboration with other services, and everything that makes the app unique and useful to people. That is the app’s business logic.

The requirements for how business logic should be structured is familiar: there should be small objects that collaborate with each other through exchange of messages. Each object should have a single responsibility, a limited and concise public interface.

Moreover, these objects should not be coupled with Rails. The layers from the framework are already assigned other tasks related to persistence, presentation, and transport. The business logic should be decoupled from external libraries as much as possible, as it specializes in the universe the app is featured in.

For that, we will introduce new layers to the app stack: repositories, inputs, models, actions, and results. To better explain these concepts, let’s start with a simple example of Rails code: a classic blog app. This code is similar to the Blog example found in the Rails Guides.

In the example, there is one Active Record for the table of articles, with a couple of validations:

# app/models/article.rb

class Article < ApplicationRecord
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

Articles Controller interacts with the Article Record to instantiate, fetch, and persist data. This code was generated by the Rails scaffold command:

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :edit, :update, :destroy]

  # GET /articles
  def index
    @articles = Article.all
  end

  # GET /articles/1
  def show
  end

  # GET /articles/new
  def new
    @article = Article.new
  end

  # GET /articles/1/edit
  def edit
  end

  # POST /articles
  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article, notice: 'Article was successfully created.'
    else
      render :new
    end
  end

  # PATCH/PUT /articles/1
  def update
    if @article.update(article_params)
      redirect_to @article, notice: 'Article was successfully updated.'
    else
      render :edit
    end
  end

  # DELETE /articles/1
  def destroy
    @article.destroy
    redirect_to articles_url, notice: 'Article was successfully destroyed.'
  end

  private

  # Use callbacks to share common setup or constraints between actions.
  def set_article
    @article = Article.find(params[:id])
  end

  # Only allow a list of trusted parameters through.
  def article_params
    params.require(:article).permit(:title, :body)
  end
end

Finally, the views invoked by Articles Controller renders instances of Article Record to list their contents, and to generate HTML forms. For example, the index view lists all Articles:

<p id="notice"><%= notice %></p>

<h1>Articles</h1>

<table>
 <thead>
   <tr>
     <th>Title</th>
     <th>Body</th>
     <th colspan="3"></th>
   </tr>
 </thead>

 <tbody>
   <% @articles.each do |article| %>
     <tr>
       <td><%= article.title %></td>
       <td><%= article.body %></td>
       <td><%= link_to 'Show', article %></td>
       <td><%= link_to 'Edit', edit_article_path(article) %></td>
       <td><%= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' } %></td>
     </tr>
   <% end %>
 </tbody>
</table>

<br>

<%= link_to 'New Article', new_article_path %>

The new view, for instance, renders an HTML form to allow users to write new Articles:

<h1>New Article</h1>

<%= form_with(model: @article) do |form| %>
 <% if @article.errors.any? %>
   <div id="error_explanation">
     <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>

     <ul>
       <% @article.errors.each do |error| %>
         <li><%= error.full_message %></li>
       <% end %>
     </ul>
   </div>
 <% end %>

 <div class="field">
   <%= form.label :title %>
   <%= form.text_field :title %>
 </div>

 <div class="field">
   <%= form.label :body %>
   <%= form.text_area :body %>
 </div>

 <div class="actions">
   <%= form.submit %>
 </div>
<% end %>

<%= link_to 'Back', articles_path %>