Domain Logic in Rails

What is Domain Logic?

All software has a domain in which it attempts to solve problems. This can be equivalent to a real-world business or to a field of engineering or math. The domain logic is the representation of the business rules of the domain being modeled. In object-oriented software engineering, a domain object model is used to model problem domain objects, or objects that have a direct counterpart in the problem domain. [1]Traditionally, domain logic in Ruby on Rails is modelled in ActiveRecord models. ActiveRecord is the default object-relational mapping (ORM) library for Rails. Hence, objects that represent domain logic are also responsible for persisting or saving data attributes to a relational database.

The Great Debate

There are two radically different schools of thought on combining domain logic with a persistence layer or ORM. First, we have Uncle Bob, one of the authors of the Agile Manifesto:

“The opportunity we miss when we structure our applications around Active Record is the opportunity to use object oriented design.” [2]

The context of that quote is about the confusion caused when you have objects that also happen to be data structures. Objects in object-oriented programming are diametrically opposed to data structures. Objects expose behavior while data structures have no behavior.Then, we have David Heinemeier Hansson–creator of Rails, proponent of opinionated software:

@dhh — It’s great to hear your comments on the latest memes in Rubyland/web dev. What are your thoughts on separating logic + persistence.
— Paul Campbell (@paulca) June 29, 2012

From this tweet we can see DHH’s opinion that a Rails application is a web app and nothing else. We shouldn’t be concerned about separating non-application specific business logic from our framework. We shouldn’t need to re-use our domain logic outside of Rails.So we have a dilemma. Should we follow Uncle Bob’s advice and isolate our domain logic from our database layer? Or should we keep on doing things the Rails way and use the active record pattern (which also happens to be named ActiveRecord) to both model behavior and persist data?

The Problem with Rails

In many cases, DHH is right. Our little pet-project blog app is nothing more than a blog. Its business logic is intimately tied to the delivery platform—the web. There is no need to separate out the domain logic. It’s just a simple CRUD app.But what happens when your app is more than just a blog? What if your application needs to model complex behavior—say, a payroll app. You have dozens of models and your tests start to take longer and longer with complex database interactions. Then you start wanting to separate out business logic by extracting domain objects. You mock or stub out your tests and they are moving fast again. But still, your views and controllers are tightly coupled with your models, reaching far inside to extract data. It can’t all be tested easily except by huge integration tests.If those problems sound all too familiar, you might be ready to re-discover some practices and approaches that have worked well in the history of software engineering.

The Solution: Object-Oriented Design

It just so happens that what’s needed to speed up our tests is the same things that are good for object-oriented software design. There are a handful of time-tested approaches that can make our applications faster, easier to maintain, and more bug-free. The first principle is single-responsibility. Each class should be responsible for a single concern. Put another way, classes should only have a single reason to change. If a class is responsible for too much it will require frequent changes and make the application more fragile.Rails’ models, which inherit from ActiveRecord::Base, are responsible for both domain logic and persistence. This is one responsibility too many. So the first step in refactoring a fragile Rails application is putting all domain logic in Plain Old Ruby Objects (POROs). Free from the constraints of the database we can now model our domain in a fully object-oriented way. We can use object-oriented design patterns such as inheritance, facades, decorators, and so on. For a full description of many such design patterns a good reference is Design Patterns: Elements of Reusable Object-Oriented Software.

Dependency Inversion

The next approach we will explore is creating a boundary that inverts the dependencies between controllers, views, and models. According to Uncle Bob’s Clean Architecture, software dependencies should all point towards higher-level business rules. The database, web, and other external interfaces thus become the lowest-level implementation details. This is the reverse of how Rails applications are often designed. Usually, the database design is considered first and then ActiveRecord models are created with a near one-to-one mapping to database tables. Clean architecture turns that process upside down and has you consider your domain logic far ahead of the database.Once you have a test-driven domain object model that begins to fully model and implement your business rules, you can begin to focus on the interfaces that control the boundaries between the web and the actual domain logic.

Use Cases

The interfaces that interact with the database, web, external services on the one side, and your domain entities on the other side, are called Use Cases. A use case can also be described as a User Story. A use case defines the interaction between a role and part of the system. There are special syntaxes for describing use cases, such as Cucumber steps. But in simple form a use case can be described in terms such as “a user logging in”, or “a manager adding a new employee”.In Rails, use cases can be implemented through Interactor models that take request objects passed in through the controller and return response objects that get rendered in the view. The request and response objects are simple data structures with no behavior or logic. Interactors can thus interact with controllers, domain objects, and repositories.

Repositories

At this point you may be thinking we’re throwing all of Rails out. But we’re not. In fact, we are going to keep on using ActiveRecord::Base as the base class for our repository models. The name repository implies a data store. So these repository models are responsible only for storing and retrieving data. We can use the full Active Record query interface if we want to, or drop down into plain SQL. It doesn’t matter how we implement repositories as long as our dependencies point away from them. With inverted depencies our application is loosely coupled and we can freely refactor domain logic without caring about the implementation details of the persistence layer.

Clean Rails Diagram | Smashing Boxes Blog

An Example in Rails

We tried this approach in a Rails app that had already been under development for a few months. The complexity of the domain led us to the decision to refactor away from the traditional Rails way towards this type of clean architecture. Immediately the benefits could be felt when running tests. The old tests were already taking about 30 seconds without too many cases. The new tests run in about a second and we can now freely add new ones without fear of slowing them down. Our domain logic was completely refactored from a database-centric one to a properly object-oriented one. This simplified the many table joins we had to do previously. Now we use object inheritance to group similar concepts and can get the behavior we need without complex database queries.

How it’s Done

First, I started off in a new clean directory and added a lib/ and spec/. Using test-first development, I started creating entities. Entities are where all the domain logic lives. They inherit from a base class called Entity.class Entity  include ActiveModel::Validations  attr_accessor :id  def self.attr_accessor(*vars)    @class_attributes ||= [:id]    @class_attributes.concat vars    super(*vars)  end  def self.class_attributes    @class_attributes  end  def self.inherited(sublass)    sublass.attr_accessor(*@class_attributes)  end  def initialize(attr = {})    attr.each do |name, value|      send("#{name}=", value)    end  endend
I’m interested in the class and object attributes because it’s convenient for validations.Next, I implemented the Interactor base class.class Interactor  attr_reader :request, :response  def initialize(request = {})    @request = request    @response = Response.new  end  private  def save_repository(repo_class)    if @request.params[:id]      repo = repo_class.find(@request.params[:id])      repo.update_attributes(@request.object_attributes)    else      repo = repo_class.create(@request.object_attributes)    end    repo  end  def delete(repo_class)    repo = repo_class.find(@request.params[:id])    repo.destroy  endend
Notice there is a convenience method there used to save data into a repository. It can handle either create or update, depending on the params passed by the controller.What do the request and response objects look like?These have some behavior that make it easy to integrate Rails form helpers. For example, a response object that contains an id will be considered as a persisted record by a Rails form and so the form action will be the update url rather than the create url. They both are Hashie::Mash objects because that makes accessing attributes cleaner.Now how does this integrate with Rails? It’s simple. After I finished the basic domain object model and a couple of interactors, I dropped them into the Rails app directory. Be sure to add paths to entities and interactors in your autoload paths in config/application.rb. The application controller loads commonly used data attributes into every request:class ApplicationController < ActionController::Base  before_filter :set_default_request  def current_account_id    @current_account_id ||= current_user.account_repository_id if user_signed_in?  end  helper_method :current_account_id  protected  def set_default_request    if current_user      @request = Request.new(current_user: current_user,                             current_account_id: current_account_id,                             params: params)    else      @request = Request.new(params: params)    end  endendYou’ll see there are still some ActiveRecord associations being used, partly in order to make Devise work. A user belongs to an account and both have their data stored in repositories. So what does a repository look like? Very simple, no logic:# == Schema Information## Table name: user_repository##  id                       :integer          not null, primary key#  account_repository_id    :integer#  first_name               :text#  last_name                :text#  state                    :text#  email                    :string(255)      default(""), not null#  encrypted_password       :string(255)      default(""), not null#  reset_password_token     :string(255)#  reset_password_sent_at   :datetime#  remember_created_at      :datetime#  sign_in_count            :integer          default(0), not null#  current_sign_in_at       :datetime#  last_sign_in_at          :datetime#  current_sign_in_ip       :string(255)#  last_sign_in_ip          :string(255)#class UserRepository < ActiveRecord::Base  self.table_name = 'user_repository'  belongs_to :account, class_name: 'AccountRepository', foreign_key: :account_repository_id  devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackableendNow controllers simply route traffic and call interactors to perform the actual use cases:class UsersController < ApplicationController  respond_to :html  before_filter :authenticate_user!  def user_params    params.require(:user).permit :first_name, :last_name, :email, :password, :password_confirmation, :phone  end  private :user_params  def index    @response = BrowseUsers.new(@request).call.extend(UserPresenter)    respond_with @response  end  def show    @response = LoadUser.new(@request).call    respond_with @response  end  alias_method :edit, :show  def new    @response = NewUser.new(@request).call    respond_with @response  end  def create    @request.object_attributes = user_params    @response = SaveUser.new(@request).call    respond_with @response.user, location: users_path  end  alias_method :update, :create  def destroy    @response = DeleteUser.new(@request).call    respond_with @response, location: users_path  endend
We make use of presenters to handle presentation details as well. The call method on an interactor instance always takes a @request object and returns a @response object which can be a nested data object. The views only know about what’s in @response. It is the interactor’s duty to get the data required for the view from the repository. Views know nothing about ActiveRecord, entities, or the database. Repositories know nothing about business rules or domain logic.Finally, here’s a set of interactors that can handle simple CRUD operations:class LoadLocation < Interactor  def call    @response.location = location.attributes    @response  end  def location    LocationRepository.joins(:manager)      .select("*, user_repository.name AS manager_name")      .where(account_repository_id: @request.current_account_id)      .find(@request.params[:id])  endendclass LoadLocations < Interactor  def call    @response.locations = locations    @response  end  def locations    LocationRepository.joins(:manager)      .select("*, user_repository.name AS manager_name")      .where(account_repository_id: @request.current_account_id)      .map(&:attributes)  endendclass SaveLocation < Interactor  def call    location = Location.new(@request.object_attributes)    if location.valid?      repo = save_repository(LocationRepository)      @response.location = repo.attributes    else      @response.location = @request.object_attributes      @response.location.errors = location.errors    end    @response  endendclass DeleteLocation < Interactor  def call    @response = delete(LocationRepository)  endend

Conclusion

This is a very different way of doing things as far as Rails goes. It offers a clean boundary between application logic and implementation details. It would be quite easy to refactor an app like this into a Sinatra app. Or it could even use a different language for the delivery mechanism. External services should be easy to plugin. Likewise for database backends and ORMs. But most importantly, implementing a Rails app using a clean architecture reminds us to keep up the red-green-refactor TDD cycle and to make sure our models only have a single responsibility. Yes, it is more work this way but this is a case of not being penny wise and pound foolish. We can build seemingly powerful applications very quickly in Rails. But if we want to keep maintaining and refactoring and adding features for years, that initial productivity boost that Rails affords us will quickly run out and leave us with a big ball of mud.Comment below with your thoughts.Cover image by Alan Levine

Subscribe to the Smashing Boxes Blog today!

Smashing Boxes is a creative technology lab that partners with clients, taking an integrated approach to solving complex business problems. We fuse strategy, design, and engineering with a steady dose of entrepreneurial acumen and just the right amount of disruptive zeal. Simply put, no matter what we do, we strive to do it boldly. Let's talk today!

Related Posts

The Foundation of Digital Transformation

Digital Transformation -- the pivotal process of using emerging or new technologies to modify or improve how your organization or product functions; improving your business processes, corporate culture, and customer experiences to align with evolving market dynamics.

view post

Cracking the Blockchain Code: A Comprehensive Guide

Discover the intriguing world of blockchain technology in this comprehensive guide. Unveil the inner workings of blockchain mining and gain an understanding of how this revolutionary technology is transforming financial transactions.

view post