5 useful concepts of Ruby on Rails

5 useful concepts of Ruby on Rails

Learn how you can build better production ready applications with Ruby on Rails as a beginner

Play this article

You may ask "Why would anyone want to work on something so old as Ruby on Rails even if there are newer alternatives available?" - I used to ask myself this same question when I started working on Ruby on Rails.

But then eventually as I started contributing to the project, I started loving this awesome framework.

I have worked on the RoR framework for over 2 years, and here I am sharing the experience and knowledge I have gained working on production applications using RoR.

Why Ruby on Rails 💡

The biggest competitive advantage of using Ruby on Rails over other frameworks is the vast ecosystem, framework maturity and speed of product development.

On top of this, it has lots of supporting libraries, which will help you even more in your development journey.

It is known for its easy syntax and rapid programming so that you can focus on more important aspects like system designs, integrations and other engineering principles like testability and maintainability.

With all the pros above, it also comes with its challenges like slow runtime or slow performance at very high throughputs, hence it is mostly preferred by startups who are still scaling up and trying out different things and shipping features fast.

Let's start with the concepts 💫

First things first, I will start with a very common use case, upon which all further concepts will be explained. Let's consider the backend of a "product catalogue service" in an e-commerce company. This backend service deals with

  • Product creation and updation

  • Exposing product details to the outside world or other services within organizations via APIs

  • Broadcasting the product updates asynchronously via some message-passing mechanisms.

Let’s create a new rails project using the command:

rails new catalog

Note: For demonstration purposes, I will be using MySQL database and ActiveRecord ORM for interacting with the database.

Don't know what is ORM ? - Object-relational mapping (ORM) is a mechanism that makes it easier to access and manipulate database objects without actually writing raw SQL queries.

1. MVC (Model-View-Controller)

This is the most common design pattern that many frameworks support, including Laravel, Spring MVC, Ruby on Rails, etc.

Putting it most simply, MVC is a software design pattern that divides the application into three interconnected elements, namely Model, View, and Controller. The basic idea is that each application consists of these three elements-

  1. The model holds the data

  2. The view makes the application look nice with clean layouts and consistent data display

  3. The controller controls the flow between the Model and View, i.e. how the data flows through and out of the application.

MVC pattern allows having maintainable and segregated code, which can work wonderous in making further changes as the application scales from MVP to large-scale systems.

Use case:

Let’s take this use case where the user wants to get product information. When they click on a particular product, they are directed to the PDP(Product Details Page) of that product.

Now you may think it as simple as exposing a GET API with endpoint /products/{product_id} and when the request comes to this API, just hit the producttable in the database and get the results and show it on the web page. Well, the approach is straightforward, but with the MVC pattern, you can make these requests handling more maintainable and clean.

With rails and MySQL installed in your system, just navigate to your project folder using the terminal and run the below command -

rails generate model Product name:string

This command will create a model file named product.rb in the app/models folder and will also create a migration file in db/migrate folder, which will look like the below-

Model file (product.rb)

class Product < ApplicationRecord

Migration File (_create_products.rb)

class CreateProducts < ActiveRecord::Migration[5.2]
    def change
        create_table :products do |t|
            t.string :name


Now, you need to run this migration to be able to create the actual product table in the database. You can also add some more fields to this file if you need some additional attributes in the product table. After the required changes are done, run migrations using the command -

rake db:migrate

This will create the actual table in the database.

Now since you are all set with the tables and model in hand, let us create a controller.

Go to the terminal and run the below command

rails generate controller Product

This will create a product_controller.rb file inside app/controllers folder with the following content:

class ProductController < ApplicationController

It will also create an empty app/views/product folder, which you can use for defining views for our application. You can also use this file along with any routing mechanisms for developing full-stack web applications.

So now instead of maintaining your code in a single file or making custom secondary files and handling processing between each of them, this MVC architecture lays down a common and sleek code structure and design pattern that you need to follow for making your application code more maintainable.

One of the principles of Ruby on Rails is the ‘Fat Model, Skinny Controller’, which means making the controller as simple and atomic as possible and providing all transformation and processing logic in the model. I will be using this same principle most easily in the next few concepts.

2. Trailblazers

This is a concept that has been used widely across many organizations for designing and implementing business logic layers into comprehensive and maintainable code.

As their website quotes-

“Trailblazer’s file structure organizes by CONCEPT, and then by technology.”

They seriously mean it!

The approach of writing, dividing and structuring implementation code into concepts and practising modularity at the code level is the best thing for which you will thank yourself once the production application grows big time.

Use case:

Let’s say you have a product availability logic which determines whether any product is available in the inventory at a specified city or not.

Let us assume that availability logic requires read access to 3 to 4 tables and also requires some Redis calls to fetch related data, and at the end requires processing at the application end before giving the actual availability.

Now you want to do all these things every time someone hits Catalog interface services or APIs. So, for any requested product in any city, the catalog must provide actual availability precisely.

Implementing all of this at the Model Level or Controller Level alone is a bad practice as the logic may talk to several models and also it may scale in the future. This is where the Trailblazer concept can come into the picture.

A sample trailblazer operation file for calculating availability will look like this-

class ProductAvailability < Trailblazer::Operation
    step :read_params
    step :fetch_product_and_location_details
    step :calculate_availability
    step :persist_and_build_output

    def read_params(options)
    # logic to read the product and location id params passed
    #if params not passed, return false (this will exit the concept and return to the caller)

    def fetch_product_and_location_details(options)
    # hit db/cache to fetch location and product details

    def calculate_availability(options)
    # logic to calculate availability for a product-location

    def persist_and_build_output(options)
    # store the availability calculated and return the response

You must have observed a parameter named options in the above file. It is the parameter of the type map, which is common to the whole concept.

So you can assign some values to this hashmap in the first concept function and read that in the next one, and also return the values to the caller. Example: you can assign product details to options[:product] in the second function and access it in the third function, you can assign the final response to options[:result] and return, and refer to this result from the caller.

So options value can look like this:-

options = {:product: {:id:123, :name: "P1"}, :result: {:availability: "IN_STOCK"}}

Also, as specified in the first function, returning false from any of the "step" stops the current flow of concept and returns to the caller with the values added to this options variable till that time.

Apart from operations, there are other related functions that this awesome framework provides. You can explore them here- http://trailblazer.to/.

3. Active Jobs

This is a framework where you can declare async jobs and run them on any queuing services. Jobs can be used for -

  • Runnin cron / daily cleanup tasks

  • Sending events to a couple of services

  • Running some calculations or processing some update event of a record in the model.

Active jobs run parallel and in a separate thread from the current main application thread.

Rails provide this Active Job framework where you can create a job, enqueue the jobs on certain events, specify a queue where the job should get enqueued, execute it, and many more functions.

Use case:

Let’s say that the catalog service gets the product details from another microservice through a Kafka queue, processes it and persists it in its database.

Some attributes of the product might also influence the availability of that product(like if a product is discontinued or banned), so you need to call the availability concept that we saw in the earlier example, to recalculate the availability for that product, and persist it back in the database.

This should be done parallelly so that the current application thread is not strained or blocked with additional overhead.

To do this, create a new file  app/jobs/recalculate_availability_job.rb, with job name as RecalculateAvailabilityJob. Accept product_id parameter to this job and in this job, call the availability concept which we saw in the earlier use case inside this function. Snippet added below-

class RecalculateAvailabilityJob < ApplicationJob
    def perform(p_id)
        ProductAvailability.call product_id: p_id

Now, in your Product model, use an after-commit hook to trigger the above job whenever the product's discontinued attribute is changed.

class Product < ApplicationRecord
    after_commit :recalculate_availability

    def recalculate_availability
        if discontinued_previously_changed?
            RecalculateAvailabilityJob.perform_later id

Here, perform_later is the keyword that enqueues the job to be performed as soon as the queue gets empty.

So with the above code, whenever an attribute discontinued of any product is changed, this job gets enqueued which gets processed by a different thread and it calls the availability concept for the same product whose attribute was changed.

4. Mailers

As the name suggests, this script is used to send emails.

Use case:

You want to notify certain people about a product whenever it is banned.

Just create banned_product_mailer.rb in app/mailers/ folder as below-

class BannedProductMailer < ApplicationMailer
    def send_mail(name:, email_id:)
        attachments['banned_products.txt'] = { mime_type: 'text/plain', content: name.join("\n") }
        mail(to: email_id, subject: "Banned Products Alert: #{ENV['RAILS_ENV']}") do |format|
        body = "PFA #{result.count} banned/unbanned products"
        format.text { render plain: body }

Now, in your Product model, use another after-commit to send an email when the banned attribute of the product model is made true.

class Product < ApplicationRecord
    after_commit :send_mail_on_banned

    def send_mail_on_banned
        mail_ids = JSON.parse(ENV['BANNED_PRODUCT_MAIL_IDS'])
        if banned_previously_changed? && banned_previous_change[1]
            BannedProductMailer.send_mail(name: name, email_id:)

5. Rake Tasks

These are the one-time scripts that you can write once and deploy, and then execute via console whenever you need to run it. These tasks are written for a specific purpose like performing data cleanup, updating some data, performing indexing of all the records, etc.

Since the developer doesn’t have access to manually update the production data, these kinds of tasks are very useful if there are any cleanups, small data anomaly fixes or some ad-hoc tasks that need to be run

For example, I developed a rake task to update any column value of a table. This had been used several times by our team for updating values for some ad-hoc requirements as well as for critical short-term data anomaly fixes.

Similarly, you can write rake tasks for your use case to provide solutions for some of the common problems.

To write a rake task, create a directory named tasks or scripts and inside this directory, create a file for a task which you need to run with .rake extension.

Sample task: hello_world.rake

desc "Print Hello world"
task :print_hello_world do
    puts "Hello World!"

Run this task via the terminal using the below command

rake print_hello_world

🌟 Bonus 🌟

Since you have read the article up to here, here is a small bonus.

Below are the most commonly used Ruby's in-built functions and objects that I have found most useful and have used extensively while working on the project.

1. map, split, join, flatten, etc - How to Use The Ruby Map Method (With Examples)

2. procs and lambdas - Ruby Blocks, Procs & Lambdas - The Ultimate Guide!

3. permutation

4. combination

Apart from these, there are many more functions, objects, and block patterns in Ruby, which you can explore and implement in your applications, but the above four are the ones which I have found most useful and used 80% of the time.

If you found this article helpful, please like, comment and share this article so that it reaches others. 😃

For more such content, please subscribe to my newsletter so that you get an email notification on my latest post. 💻

Let's connect 💫. You can follow me on