Ruby on Rails — Getting the code structure right

Ankit Tomar
Bizongo Tech
Published in
5 min readMay 14, 2018

--

Image Courtesy

Every Rails developer would have heard this multiple times “Skinny controllers and Fat Models”. People always recommend this approach amongst the top 10 best rails practices. Though Skinny controllers sound good to me, I have always cringed at Fat Models. Models at the end should only describe your constraints and should ideally not contain a lot of functionality. Then how do you keep your controllers and models skinny. All that logic, talking to external api’s, code processing, mailers, workers and other app level logic, where should all of that go.

The answer is to open your default rails structure and customize it. At Bizongo, we quickly realised that for the complexity we have across our products, a simple MVC structure was not enough. For code readability as well as modularity we needed to make sure we have structured our code in such a way that any new person could also decipher where they could find a particular piece of code. Other than the basic model view controllers, we adopted the following -

  1. Service Layer — DRY controllers

We use controllers only to delegate API calls to the service layer. Service layer takes care of running application logic and returning return parameters. A controller for us always looks like the following

def index
instance = CatalogServices::Category.new(params)
instance.execute!
render json: { categories: instance.result }, status: :ok
end

A simple delegation to the service which then executes logic and returns the result. A service on the other hand has the following structure

class CatalogServices::Category < BaseService

=begin
request_context = {
key1: "value1",
key2: "value2"
}
=end
def initialize(context)
@context = context

@add = nil
@diff = nil
end

# This should return true or false
def execute
return false unless super

@add = @context[:key1] + @context[:key2]
@diff = @context[:key1] - @context[:key2]

valid?
end

protected

# This protected api can be used to run validation checks
def validate
super

error('key1 is not present') if @context[:key1].blank?
error 'key2 is not present' if @context[:key2].blank?
return unless valid?

error 'key1 is not int' if @context[:key1].to_i != @context[:key1]
error 'key2 is not int' if @context[:key2].to_i != @context[:key2]
end

end

Service is divided into validation and execution where first all validations are run and then logic is executed to return the result. This is a customised service module which inherits from a BaseService class written specially by us to make it easier to generate services. We will discuss this in detail in another blog post and add it here later

2. Event Driven Architecture — Publish and Subscribe pattern

When you have a complex web of notifications you need to send after certain action, it becomes increasing difficult to manage where to send which notification from. After a while when an action is happening from multiple places you will end up calling notifications from models, controllers and callbacks. This makes code highly unscalable as it creates duplicate code and makes it very difficult to track. It helps to start thinking about decoupling logic and after action logic at this stage. We adopted publish, subscribe to solve this and make code more structured and scalable for us.

Eg — if you have multiple flows to place a customer order, you don’t need to trigger notification from all flows. The logic will just trigger a order placed event broadcast and listeners who are subscribed to those events will perform all after action logic which needs to happen including notifications.

A broadcast call happens in the following manner

Bizongo::Publisher.broadcast(:order_item_accepted, self,       
send_notification: send_notification,
user: user)

We use Wisper to manager Publish-Subscribe capabilities for us.

3. Concerns — DRY up models

The Concern is a tool provided by the ActiveSupport lib for including modules in classes.

Concerns are a great way to DRY up your model if you are actually using the same functionality in more than one model. It lets you create a separate module which can then just simply be included in various models.

A pretty well known example of this is as below

# app/models/product.rbclass Product include Taggable ...end# app/models/concerns/taggable.rb
# notice that the file name has to match the module name
# (applying Rails conventions for autoloading)
module Taggable
extend ActiveSupport::Concern

included do
has_many :taggings, as: :taggable
has_many :tags, through: :taggings
class_attribute :tag_limit
end
def tags_string
tags.map(&:name).join(', ')
end
def tags_string=(tag_string)
tag_names = tag_string.to_s.split(', ')
tag_names.each do |tag_name|
tags.build(name: tag_name)
end
end
# methods defined here are going to extend the class, not the instance of it module ClassMethods

def tag_limit(value)
self.tag_limit_value = value
end
end
end

Reference -

https://signalvnoise.com/posts/3372-put-chubby-models-on-a-diet-with-concerns

http://api.rubyonrails.org/classes/ActiveSupport/Concern.html

4. Global Error handlers

In your entire project you will have to do a lot of error handling and after a while it becomes painful to always add begin rescue blocks everywhere. Error messages and types also become inconsistent once difficult people starting throwing different errors around the entire codebase. So, we thought why not allow everyone to throw errors wherever they want in the code and handle it globally once before returning the response. So, we created Global error handler as a concern which is included in the base API controllers from which each of the controllers inherit.

5. Model Scopes

Models scopes are another way of making sure we are not writing long queries in services repeatedly. A scope once defined in a model to get active records for that model can be then used anywhere, making code more structured and modular. An example of a model scope is

scope :delivered_orders, -> { 
where(status: Order.delivery_statuses[:delivered])
}

We hope the above will help the reader start a rails project better with modularity and scalability. If you are passionate about Rails and want to join us in making this even better Get in touch with us on Bizongo Careers. We will soon be coming up with more detailed blogs on each of the above points.

--

--