5 Opinionated Tips for Creating a Maintainable Rails Application

Unathi Chonco
8 min readMar 13, 2019

--

I’ve spent just over a year working on one Rails application. The project has had 4 developers, and about the same amount of designers have contributed to a variety of features.

From having the opportunity to work with a more senior developer, mentoring junior developers, and collaborating with a few designers, I’ve seen the impact certain decisions can have on what we produce as a team.

The team has now been scaled down to just 1 developer who, ideally, needs to be able to understand, test, and iterate on the existing codebase.

Here are a few tips which I believe have been helpful, or would have been, to create a maintainable codebase:

1. Create, and maintain a living style guide

A living style guide acts as a great visual, and code, template for how to implement specific UI components and patterns that occur in your application. Ideally, the designer should keep the library of visual components in the application up to date, and the developers maintain a live version of this library.

When a team is iterating fast on a project, they can be much more productive when they no longer need to make decisions on how to implement components of the UI which are repeated throughout the application. The application should keep a standard implementation of these common components, helping to keep consistency in the codebase.

There is a continuous time-cost to maintaining this style guide, but the return on investment can be extremely beneficial to the velocity, productivity, and consistency of the team’s work.

I’ve found that styling is a weak point of many junior to mid-level developers, there are endless possibilities when trying to get something to look “right”. But to have a codebase that is more enjoyable to work on, building consistency in common UI elements is important.

2. Enforce consistency in your stylesheets

CSS is something that can become messy very very quickly. Styling has many different approaches to achieve similar results. It can be tough, and maybe unnecessary, to enforce the approach of how applications should be styled, but a few rules can be enforced.

The first two steps I believe provide a large return on investment when it comes to the maintainability of the codebase, are:

  1. Organised CSS files
  2. Fixed convention on naming

There are countless methodologies that can be applied in order to achieve the above. Whichever methodology you choose to go with, you will benefit from having more structured, understandable, and organised CSS and UI in your codebase.

The 2 methodologies, and file organisation systems, I have experienced to work well are ITCSS and BEM.

Applying these conventions in an application has proven extremely useful in the use, and reuse, of styles. When applied consistently, even “messy” implementations can be refactored easier, because the components will often be self-contained entities with little side effects to the components around it.

3. Keep your models skinny

Models are a core concept in an MVC framework, which may consequently encourage people to attach all logic related to models to the model objects themselves. In a Rails project, this can work very well early on, but it can quickly cause models to become bloated with business logic beyond the scope of what I believe is the central responsibility: simply reading and writing to database tables.

Here’s a simple example:

This may not seem too messy just yet, especially considering that the logic of #managed_admin_roles isn’t complex. But let’s consider the inevitability of this model file accumulating 10–20 more methods with unique and more complex business logic… that becomes a big and ugly file.

In instances like this, I prefer to extract such business logic into Service Objects or Query Objects. The implementations of these can vary, but the goal remains: extract the logic out. Extraction of logic can make it easier to understand what’s going on and have a smaller unit of code to test.

The method #managed_admin_roles can be extracted into a service object that looks something like this:

Refactored User model
Service Object with extracted logic

I like to think that when complex logic is contained, it’s still manageable and maintainable even if the code is messy at first.

This happens because these service objects are small units of code concerned with achieving only one thing. I find practising the extraction of logic like this feels very natural in the test-driven development world, following the “Red, Green, Refactor” mantra:

  1. Red: Write failing tests for your service object, defining how you want to interact with it, and what you expect the results to be.
  2. Green: Implement the service object to get the tests passing
  3. Refactor: Clean up your implementation (and any refactoring will have no impact at all on code that uses the service object 👏)

4. Keep your controllers skinny

Controllers are what handle the execution of all business logic, and rendering a response, in a web application. Because of this, it’s very common that controllers become extremely bloated files. Even more so than models, controllers filled with a ton of complex logic easily causes you to lose track of your business rules.

Here is an example of a controller where the #create action handles subscribing the users of a business to some product.

There isn’t very complex logic in this controller, but there is a lot happening. Looking at the code, we can see that all communication with the payment processor and the CRM have both been put into wrappers.

This works, but I believe there is a lot more inside this controller than there needs to be. A controller should only be concerned with taking in the request and rendering the response to the client, and this is what we should test for each controller. When there is a lot more work we want to do, which is not concerned with the responsibilities of a controller, we can extract this to service/query objects. This will work very similar to the extraction done in our models.

The resulting controller will look more like this:

The benefit of this extraction is that no matter how complex the process of subscribing a company may get, the controller’s complexity does not increase. We should rarely need to test additional cases against our controller, keeping it clean and easy to follow.

The service object has a single responsibility, usually determinable by the name, and just the simple separation of concerns makes it easier to maintain in the future when you return to that piece of code for a bug fix or a functionality addition.

5. Keep your javascript simple

The opinions of implementing features in JavaScript are endless. Making a decision on which tools and libraries to use can be a big barrier for some because the community doesn’t seem to have strong global conventions the same way the Ruby/Rails community does. At least that’s how it often feels for those really diving into javascript for the first time.

Something to consider when looking at the maintainability of a project is the learning curve required for someone to understand, and be able to contribute to, the codebase. I believe keeping to some convention reduces this learning curve.

An easy first step is to ensure everyone contributing to the project has the JS linter’s set up, this is one simple way to get everyone writing the same style of code. (Code style consistency should be applied to all code in the project, not just javascript)

There are 2 main points I’d like to highlight which I believe have, or would have, made our javascript more manageable to work with.

Convention in implementing custom components

Sometimes we create our own interactive UI components that require some custom JS. To avoid having the implementation of these components looking like complete magic, I apply these to steps:

  1. Create custom CSS classes for those elements, which act solely as JS triggers.

For example, let’s say we want to implement some javascript that will make any component act as a link. I would call these items “linkable”, so the name of my JS trigger class would be `.js-linkable`. The prepended `js-` is just a naming convention to state that this class has no CSS styling attached to it, and is there solely for the purpose of JavaScript.
Example element:

2. For each component that has custom JS, in this case, our “linkable” components, we keep a separate JS file with the correlating name that contains all implementation logic for that component.
The name of the file matching the name of the trigger makes it easy for one to find, and by keeping this kind of convention a developer will always know where to look when they see an element with some `js-` trigger class.

Stimulus is a modest JavaScript framework which I’ve found very appealing for cases such as the above. It enforces a convention that feels natural to Rails and does not have much of a learning curve. Which leads me to the next point.

Beware of all the fancy schmancy frontend frameworks out there

The JS world has a plethora of frontend frameworks available, and they’re great. Libraries like React, Angular, and Vue can be extremely helpful… but I’ve found that, unless you’re building a frontend application separate from your backend, they often over-complicate the codebase and you could often get away with your plain old full-stack Rails application.

This point is short but important. As web developers, we have countless different technologies at our disposal. There are new frameworks and libraries coming out on, what feels like, a daily basis. There is almost never the “perfect” tool, but rather the RIGHT tool for you. A good majority of the time, the right tool is the one you know. If you’re going to use something that you or nobody in your team has experience with, strongly reconsider the pros and cons before moving forward.

We all may be lured into wanting to say, “we‘ve got React in this part of the application to do this cool thing” until a junior developer comes on and has to ramp-up on 5 different complex technologies just to make a meaningful contribution.

Conclusion

These are just a few tips I thought would be valuable to highlight. There are many more, and the learning never stops.

Each of the points I’ve written about should be taken with a grain of salt and should be applied with consideration of the project and team at hand.

Generally, what I consider one of the most important rules to apply (which is applied in all the above points), is CONSISTENCY. It’s always easier to understand things that follow a regular pattern consistently. Keep the team consistent, and it will usually be easier for developers of any level to ramp-up on a project.

Please share your own opinions, and tips, that you find valuable when working on a production application.

--

--

Unathi Chonco

Building Knowledge 📚; Building Products 👨🏽‍💻; Building People 📈