Common mistakes in Project Ruby on Rails
As a Ruby on Rails (ROR) developer, you have probably heard that "Ruby on Rails will not be able to meet the development needs of an Enterprise-level project," right? However, I do not agree with this viewpoint because, in the current Enterprise-level project, where micro-services (SaaS projects) are preferred, it is not advisable to limit a project to a single language, regardless of the language itself.
However, I still to be able to understand why ROR may not be as popular for Enterprise-level projects. One of the common reasons I often come across is that ROR developers sometimes make mistakes from the beginning when building and developing projects. Below are some common mistakes I frequently observe when working on a ROR project.
1. Not following Code convention
Imagine Ruby as a human language and Rails as a country, so when you live in a new place, how would you start?
My approach is as follows: First, I would learn basic words and then try to learn the pronunciation and grammar. The next step, I need to obey the laws and use the commonly spoken language in that country with some general conventions to facilitate living and working.
In programming, my approach is the same: Learn the basic syntax and then follow the code conventions in the program language and framework before trying to use them for programming. Following conventions from the beginning helps me communicate easily within the code with other developers. Besides saving a lot of time in reading and understanding code, following conventions also helps me avoid mistakes in the code.
For example:
In work, my team has some conventions for naming functions as follows:
Use nouns or noun phrases for functions with clear output other than true/false.
Use verbs or sentences starting with verbs for functions with true/false output or only return. (edited)
def params_after_processed
# return params
end
def process_params
# return
end
With this rule (convention), you can see that our team will have no trouble finding functions and can easily determine the output of each function without having to read through all the lines of code.
2. Not defining responsibilities for each layer
The strength of Ruby in Rails is rapid development with the MVC model. Additionally, we also usually implement layers such as Service, FormObject. However, after dividing the class, most of us ignore the factor that clearly defines the task for each class that has been divided.
I used to join to rebuild from scratch in a project because it was too difficult to maintain or add any new features. And the reason for this cost is that the project has loop circles that don't have an end. For me, this is a valuable lesson for considering the need to clearly define responsibilities for each layer from the beginning.
And here are the responsibility for each layer in a project that I have implemented:
Layer's name | Responsibility | Level |
Controller | Only use for rendering and returning HTTP statuses. | 0 |
Background job, Group service | Call and catch the errors from the Service layer. | 1 |
Service | Process the business logic and call model to save. | 2 |
Validation (FormObject or Dry::Validation) | Use for validation of input data before saving to DB or processing business logic. | 3 |
Model | Only use to save DB and mapping data from DB. Additionally, we can set the validations of the relationship in DB. | 4 |
(I will have a post for the topic Layers in Rails
)
You can see that by assigning responsibility to each layer, if there are any bugs or issues, we can quickly determine where they are and their impact by tracing back from level 4 -> 0.
3. Abusing DB transactions
This is another mistake that our team made when developing in ROR and caused many incidents related to DB Deadlock.
As the Dev, we usually try to open DB transactions with very small scopes that are not worth worrying about. However, when the logic grows over time, we try to push the code to process the logic and it makes accidentally the commit time of a DB transaction become longer, leading to the consequence of DB Deadlock. After receiving some negative feedback from the boss, our team sat down to research and agree on the principles of using DB transactions:
Avoid calling 3rd party within a DB transaction.
Create and assign all data to variables and only bring the logic that depends on the record's id into the DB transaction.
Avoid calling functions within a DB transaction.
Clearly define the flow of tables and only put data that truly depends on each other within the DB transaction.
For example:
We have the context: After the user bought successfully at POS, POS will send the request with data including user info and order id, we need to get order info from 3rd party (SaleOrderService) by id and save this order if the user info did register and accumulate points based on the formula for the user. Then, we send the notice to the app of the user.
# before refactor
def submit
return if @user.nil?
order = SaleOrderService.get_order(@order_id)
return if order.nil?
ActiveRecord::Base.transaction do
user_order = Order.new(**order, user: @user)
user_order.save!
save_point_of(user_order) # calculate point based on formula
end
end
# after refactor
def submit
return if @user.nil?
order = SaleOrderService.get_order(@order_id)
return if order.nil?
user_order = Order.new(**order, user: @user)
point = point_of(user_order) # calculate point based on formula
ActiveRecord::Base.transaction do
user_order.save!
point.save!
end
end
Looking at the example, we can handle the information before saving it outside the DB transaction and reduce the waiting time for a transaction commit.
4. Refactoring without plan
The mindset of improving code is highly commendable. However, refactoring everywhere and anytime must be avoided. It will take time to test the impact after each refactoring phase.
Refactoring should only happen when you ensure the following:
The current code must have a high code coverage (our team sets the target at 90%).
The logic in the code must have clear documentation.
The impact of modifying that file must be within the allowed limits in terms of development and testing costs.
Double-check the full impact before refactoring.
5. Not caring about input/output types
It seems like ignoring the type of input or output is a habit of ROR developers because it allows for the quick development of ROR projects. However, ignoring the type of input/output actually creates technical debt in the long run.
And if you are a back-end developer, focusing on writing and exporting APIs, checking the input type before processing is mandatory to help increase the security level.
For example:
I have the authentication feature and I need a API to input OTP (6 numbers) for verify. I have the code.
def verify
user = User.find_by(otp: permitted_params[:otp])
...
end
Let's imagine what happens when I send the request like this:
URL: http://example.com/login/otp
Method: POST
Headers: { ... }
Request body: { opt: [000001, 000002, 000003, 000004, ..., 999999] }
Let's give me feedback if you cannot see the issues.
6. Conclusion
Every language has its own strengths and weaknesses. Utilizing the strengths and limiting the weaknesses of a language will help us build a project with better quality. And my answer to the question "Can Ruby on Rails meet the requirements for developing Enterprise projects?" is Yes. However, we still need to consider the goals and direction of the project for further discussion because, clearly, if you want to build an AI, ROR is not a suitable choice.