Top 5 mistakes in the early phase of rails project

Posted by Michał Brodecki on December 07, 2018

First to market. Early working prototype. Limited starting budget. Sounds familiar, right? This is often the reality of new rails applications. But hey, we got a support from our framework. We can write a working prototype very fast and deploy the app. But this is our loan. We are often making a technical debt in the early phases of development. And sometimes paying the debt is more painful that we can imagine. Here are the top mistakes that we do in the early phase of the development to save time.

Problem 1: Callbacks and model validations

When I first learned about callbacks it felt so natural to use them. The best example is a classic user story to solve:

After user registers, she/he should receive a welcome email.

And it can be easily achievable by adding to user.rb:

after_create :send_welcome_email

It feels so great and makes a lot of sense. And it’s so readable for someone who doesn’t know the codebase. But as your application grows, and more and more complicated features are introduced, you are starting to have a lot of places when you interact with User model. You probably also have few after_update callbacks. You may start to write if <something_changed> at the end of your callbacks invocations. Your tests are getting slower and slower, cause every time you instantiate the object bunch of side effects is triggered (emails, sidekiq jobs, calculations etc.).

The same thing is with a model validation. It feels quick and natural to put the validations in models. But again, when you have more and more contexts where you call your validations, it’s getting messy. You end up with lot’s of “ifs” at the end of the validation methods or you even put explicit validate: false.

Solution:

Use services/command objects/form objects/actions object. Whatever you call them. The idea is to extract validations to separate place, that is isolated and you are sure that these validations are the ones you need in the given context. And you can get rid of callbacks, cause every event that needs to happen is going to be invoked in this object.

Problem 2: DRY

DRY - don’t repeat yourself

DRY is probably the first and the easiest idea you learn about when you start. Also, it may look the easiest to use. However, there might be a cost of using it incorrectly. I have seen a lot of projects where DRY rule was so extensively used that the code was barely readable and it was extremely hard to navigate through the project. Sometimes we introduce cool abstractions to eliminate repetitions in our code. It might be deadly cause wrong abstraction is worse than a duplication. And it’s very easy to make a wrong abstraction. Especially in startup world where everything changes dynamically. There might be a situation when you notice that the super cool code that you wrote some time ago doesn’t make sense anymore. And if you are going to fix it immediately then it’s fine. But you won’t probably have time to do so, or you just put a task in the backlog to do so. And by the time you are able to pick the task, the piece of code you wanted to change would be so much bundled in the code that you would need to spend few days/weeks to refactor it.

Solution:

Don’t get me wrong here. DRY is not an antipattern. But it has to be used responsibly. I have a rule with DRY that if something repeats in a small context (in a single class) then it’s no-brainer mostly, and I’m drying this thing. But if I have two duplications in a larger context, for example in two separate controllers, then I do nothing. I observe and if I have to use the same piece of code third time somewhere else, I’m considering some extraction.

Problem 3: YAGNI

YAGNI - you ain’t gonna need it.

Sometimes we like to show off. We like to think of ourselves as awesome developers and our ego tells us we make a code that is the best in the world. Sometimes we are even so smart that we predict how our components will be used in the future, or what our client would like to have. And we often waste time with a work like that. We try to satisfy the task requirements, our ego and the future tasks requirements.

Or we are using sometimes wrong tools for simple things. I have seen that someone introduced an ElasticSearch engine just to search by fields like name/surname on the database with 100 records. I have seen a dockerized environment in the project with 6 controllers, few models and the simple DB connection. I have seen microservices that were doing stuff that could be done in the main application. Why? Cause someone thought that it would be nice to have this now, so when something change (we get more users, we plan on developing new features in the microservice) we are ready.

Solution: Well, the solution is simple. Don’t overengineer. Use TDD techniques to meet the requirement of the given task and as soon as it is green and refactored, stop. Take a minute to think of the solution but don’t try to introduce nice features for the future. Try to predict what may happen, but consider this as an option, not as a immediate task to do.

Problem 4: Cache early

Why do we cache? To boost our performance. I can’t imagine, especially e-commerce shops, to not cache their products for example. They do this to make shopping easy. Pages are loading fast, you can just explore the site without uncomfortable loading times. Problem is when you cache at the very beginning of your project to hide the performance issues. Your code is ineffective and your page loads are a nightmare. If you apply the cache on the top of it, you will gain a performance boost. But you hide the real issue under the carpet.

Solution: Use cache responsibly. If you have a performance issue, the cache is the last thing you can think about. If you do everything to optimize your request and the result is not satisfying then it’s probably worth considering a cache solution. But we all know how easy it is to write a bad query using ActiveRecord ORM so optimizing should be the first step to take.

Problem 5: Todo comments

I saved this as the last point. Why do I think it’s bad? I have seen many projects with a lot of TODO in the code. TODO improve this, TODO refactor, TODO do we need this?. I noticed that we sometimes write the todo comment just to have an excuse for writing the bad code. It feels so easy to write the code that works but it’s a bit messy and write a TODO refactor at the top of the method. Problem is, these todos are going to be there forever. And if you start using todos more and more in the codebase it’s obvious that you won’t have time to fix 200+ todo comments.

Solution:

Don’t write todo comments. If you feel that the code you wrote is just bad, refactor it. If you feel that something has to be done, put the task into the backlog with medium priority (however it may be a dead end too, just depends on how the project is developed). Another way to deal with messy places is a boy scout rule. Every time you do something around the place that might be refactored in your opinion, just do this. This way you got more chances that your codebase is in the good condition.

Summary

It’s easy to write the code. It’s easy to create a working rails app. The real cost comes with maintenance time. And if the maintenance is already painful, new features are very hard to introduce. Less time you spent in the early phase, more time you need to spend later. And as we know, the time is money.