Best practices for maintaining Rails applications

Sai Prasanth NG
Sai Prasanth NG
Partner June 09, 2019
#backendengineering

Ruby on Rails is a great web application framework that helps in fast prototyping and help quickly evolve the application logic per changing requirements. A smart engineering team would be planning for easy maintenance from the get go instead of a follow up task once the application has matured. In this post, I present some best practices from a maintenance standpoint with gem recommendations.

Code Styling

Code styling is the way we write code. When everyone follows a fixed set of coding conventions, it makes the code more readable for other developers to understand and contribute to the code. The Ruby community has come up with a set of coding styles (ruby-style-guide) and the rubocop gem ensures your code adheres to the defined conventions. The following screenshot presents the “offenses” from a sample run.

Rubocop output example

Test Coverage

Make things as simple as possible, but no simpler. -- Albert Einstein.

One of the major goals of writing test code is to thoroughly analyze the behavior of the implemented logic and refactor the implementation to be modular and intuitive. As the application becomes more complex, the cost of writing a maintainable application (aka technical debt) goes up. A test coverage tool analyzes the code that is covered by the test cases and keeps a vigil on code that is not evaluated by the test code. This also serves as a good health indicator of the code as far as automated verification is concerned.

Simplecov gem is one of the ruby code coverage libraries that shows the percent of your code is covered by tests. The user can dig into the coverage analysis file by file to identify the exact lines that are not covered yet.

Source: github repo
A sample coverage results output using SimpleCov

Running Tests

A large application can build up a good number of tests over time (I’ve seen instances where we had about a thousand unit tests written by a highly test driven development engineering culture). Ideally the running time for executing the tests should not be interfering with the productivity of the developer. Automatic and quick execution helps in giving immediate feedback to the developer as the changes are being made. Parallel tests takes advantage of the multiple CPU cores on the test execution instance and farms out the workload for faster execution.

Sequential Testing and Parallel Testing Illustration

Source: smartbear https://support.smartbear.com/testcomplete/docs/testing-approaches/parallel-testing.html

Unused Routes

Routes are the entry point to our application. As our application size grows and requirements change, we keep adding or deleting actions for a controller but tend to forget to remove the respective route when needed. This could end up as a security issue.

The traceroute gem helps to ensure that you do not have any unused routes in your rails application. It also lists the controller actions that are not reachable by the routes.

Traceroute usage example

Eager Loading to avoid “N+1” queries

ActiveRecord doesn’t load the data from database at the time the query is written, rather it loads the data when the data is required for some kind of manipulation in the code.  This approach is called lazy loading.

students = Student.where(“ age > 20 “)
puts students.first.name

The database query is not performed at line 1 where the query is defined but is performed when we try to manipulate/access the data at line 2.

Lazy loading leads to a problem when we try to load objects along with their associations.

Student.where( “age > 20 “).each do |student|
  student.subjects.each do |subject|
    puts subject.name
  end
end

ActiveRecord performs one query at line 1 to fetch all students whose age is greater than 20 and another query at line 2 to fetch all the subjects of each student. This results in a total of one plus N (assuming there are N students) queries, commonly known as the “N + 1” query.

We can avoid this by rewriting this code using eager loading as follows:

Student.includes(:subjects).where( “age > 20 “).each do |student|
  student.subjects.each do |subject|
    puts subject.name
  end
end

ActiveRecord loads all the students whose age is greater than 20 along with their subjects at line 1. The bullet gem helps identify instances where we are performing the “n+1” queries. 

Bullet gem in action

( source: http://railscasts.com/episodes/372-bullet?view=asciicast)

Error alerts

While good testing practices during the development stage help uncover a good number of bugs, we should still be prepared for users encountering errors on the live application. What's key in such scenarios is the minimization of the negative experience the users are facing. Ideally, the developers should get immediately notified so that a fix is en route.

We would like to know any errors that occur on production so that we can take action quickly before it affects many people. If we do a quick fix, we can reduce the impact of the bug.

The Exception Notification gem notifies the developers via email, slack and commonly used communication channels when an error occurs in the Rails application.

Profiling

Profiling studies the space or time consumed by the application for various operations. Typically it is used to analyze pieces of code flows that are taking longer time to execute for further optimization. The rack-mini-profiler gem in the following screenshot measures information such as the time each partial is taking to load and the queries that were executed. From a maintenance standpoint, studying the code executions that tend to be the bottlenecks of the application inform us on the throughputs we can achieve.

 

Rack Mini profiler metrics snapshot example

( source: http://railscasts.com/episodes/368-miniprofiler?view=asciicastt )

Database schema clarity

One of the problems developers face when they are being onboarded on to the team is that they have a very limited idea what data is present in each table. One place they could go to get the information is the database schema. A better approach is to provide the schema information as comments at the relevant places (ex: the model files) so that the developer has clarity and quick access to all the database table structures. This is exactly what the annotate_models gem does. In the sample screenshot you see down below, the schema information is annotated at the beginning of the file.

ActiveRecord Schema annotation example

 ( source: https://github.com/ctran/annotate_models )

Security vulnerabilities

Ideally, we want to have no security vulnerabilities for our application. Few of the common security attacks are

  • Command Injection - where the attacker tries to execute commands on the host’s operating system by exploiting the application’s vulnerabilities (or holes).
  • Denial of service (DoS) - where the attacker bombards the server with requests so that the system crashes or is unable to serve legitimate traffic.
  • SQL Injection - where the attacker executes malicious sql statements most commonly passed through through forms,
  • Mass assignment - where the attacker takes advantage of ActiveRecord and updates restricted fields such as user password, etc..

The brakeman gem helps in identifying common security threats in your application,informs you about the known security issues for the rails version in use and suggests updates to your application to an improvised version.

Brakeman usage example

( source: https://brakemanscanner.org/blog/page/2/ )

Brakeman report snapshot

(source http://railscasts.com/episodes/358-brakeman?view=asciicast )