How lucky I am to have a chance to work with a well written Rails app Yulia Oletskaya Follow Mar 8 · 6 min read

For quite a some time I was working with a small team on a Rails application. Our team wrote the app from scratch, we used a reasonable amount of patterns and best practices, but in a way that it’s not over engineered. I enjoyed the structure of the app, we had an agreement in the team on where and how we store certain service classes and structures, so it was quite easy to decide where to place new files or where to look for the code you need.

Since the team was quite small, we could easily consent on new practices, apply creative approaches, devote time to refactoring whenever we felt that a tech debt grows. It was so handy and convenient for me that I got used to the idea that this is it. This is the way people do things. This is the life of an ordinary ruby developer.

But the good cannot last forever and I was transferred to another project for a few months. And I must admit, it affected me quite sobering.

The new application was a pretty old monolith with a huge amount of legacy code. The team consisted of more than 20 developers, and this does not include QAs, PMs, POs, BIs, etc. They are all good people with a solid tech knowledge. They had a fine idea of the system as a whole. Yet when it came down to the details — nobody knew anything. I mean, nobody really knew why this or that specific decision was made. Why this service is stored in this directory, while a similar one is stored elsewhere. Why the same problem is solved by 3 different classes in different places. No naming conventions. Chaotic folders structure, while some services are stored in the /lib dir, the others are defined as nested classes, some are placed together with models, others are in the /services dir. Crazy after_save callbacks with external API calls right in the AR models. The Gemfile included dozens of gems, some of them were not even used in the app. Business logic smeared evenly on models, controllers, views, services, helpers, with significant interspersed in JQuery based scripts. Lots of heavy DB requests on each page with extra joins and unnecessary data load. Multiple package managers and significant frontend build times. And much more. Or, in other words, everyone knew the answer to any of these, and the answer to any question was: it’s like this because it’s legacy.

But that’s not bad, after all these are classic problems of many legacy apps. Everyone agreed that the technical debt is huge and it needs to be addressed at some point. Yet the main c-level technical guys were mostly focused on new contracts and implementing new parts of the system. The usual day-to-day tech debt was placed under the responsibility of the whole team. The team has a few team leads and so they should deal with it somehow. Yet it didn’t help and in this direction there was nearly no work at all (well, except some cosmetic minor improvements, but that doesn’t count). One of the main reasons why we didn’t do anything — first, we should agree on what to do, what to start from, and what’s the way we’d like to stick to, to standardize our approaches. And in such a big team it’s nearly impossible to agree on anything. There always was some kind of push back to any idea, and even in case a suggestion is minor and everyone does not mind, there was no active support (only ActiveSupport was there hehe) and all proposals inevitably drowned at the bottom of JIRA backlog.

In any case, it is a very rewarding experience to plunge into for a short while. I made a lot of conclusions for myself. And one of them is that it’s much easier to prevent the system from chaotic crawling into random and sometimes mind-blowing code constructions than to try to reorganize it at the later stages. And the other one, is that some developers could all their lives work in this fusion-style-chaotic systems, and never even see that there is another way to work on a production application. I’ve seen the “another way”, so I’m a lucky one (yay).

With this being said, now I’d like to share the way how I personally like to organize code in a Rails app.

Let’s start from the classic confrontation of /app vs /lib folder.

The /app folder contains all the stuff that belong to the business logic. We’ll get back to it later.

The /lib folder, in theory, should contain the code that later could be packaged as gems. But in the real world I usually do not know in advance, which part of the app’s code is going to be good enough to be decoupled to become a gem. So this problem is solved by the fact that I store nothing in the /lib folder. Okay, okay, almost nothing.

So, in my case, /lib folder contains rake tasks and another subfolder called developer . This one is rather specific, and not all apps need this. It stores certain media and text files which I’m using in dev-env to conveniently test different kinds of processing.

All the other folders contain configuration, database schema, migrations, logs, tests, basically all the stuff that allows the application to run but not the application itself.

Now let’s take a closer look at the /app folder itself.

Lately, a lot of developers tend to call any class which is not an AR model a service and simply throw it into /services dir, no matter what the class actually does. I‘d rather not do it. In my case services are only these classes which are making API calls. For example, S3Service or IBMWatsonService , or OpenWeatherService , you got the idea.

All the other helper classes or modules are usually can be classified as some kind of design patterns. The most common patterns I use are decorators, forms, query objects, commands, and strategies. They are stored in dedicated folders and can be re-used between f.e. controllers and sidekiq jobs.

Btw, talking about the sidekiq jobs. In my case they are rather sophisticated and the most complex processing logic is extracted to /processes folder and is stored in classes like CaptionExtractionProcess , or SyncRecordsProcess , etc., so they can be easily tested and re-used between different jobs.

All that implies that there’re almost no “fat” in models and controllers.

And yeah, it’s not completely fair to compare these 2 applications and their processes. The first one is a part of a service-oriented system, originally designed in a way that small dedicated teams can work independently. And the other one is a monolith with huge codebase in a combination with a big team without well-established processes (on their way to apply agile practices). With that being said, I personally came to a conclusion that huge monoliths are evil. Moderate-size monoliths are ok though, but this’s a topic for another story.