rails_best_practices

Rails Best Practices

Stars
6

Andy's Rails Best Practices

(work in progress)

Authored by Andy Maleh (Two-time RailsConf Speaker, RubyConf/MagicRuby/MountainWest RubyConf Speaker, and Fukuoka Ruby 2022 Special Award Winner)

Software Architecture:

  1. Smalltalk-Style MVC with smart Views pulling data instead of dumb Views with Controllers pushing data. In other words, avoid having Controllers that push tons of variables to the View with before_action hooks (even if all those hooks were extracted to modules) as that results in very inflexible unmaintainable code that does not scale well as the application grows bigger. It is much simpler to define methods that pull the variables when necessary only, which are reusable everywhere instead of just in the one controller that pushes variables, thus saving the developer from doing before_action duplications across Controllers or creating unnecessary deep inheritance Controller hierarchies.
  2. If web application pages never spawn more than 20,000 DOM elements all at once in JavaScript (99% of web applications) and have very few or no client-side-only interactions, avoid all JavaScript SPA frameworks at all costs as they do not make any performance difference (less than 20,000 DOM element updates occur in less than 500ms, thus appearing instantaneous to users, and making all over-hyped arguments about virtual-dom moot) or productivity difference and they multiply the cost of code maintenance by almost 100x times! Use "JavaScript Sprinkles" sparingly instead, using jQuery (avoid Stimulus because it includes some redundancy, wheel-reinvention, and over-engineering in the required element IDs/Classes/Controllers that could be avoided in jQuery). Also, in the aforementioned common cases, jQuery is still better, simpler, and more convenient than all JavaScript SPA frameworks, and even vanilla JavaScript despite all the ES improvements, as jQuery results in the tersest code possible and only applies updates surgically (the server generates all elements), so performance is very fast with it.
  3. In the case of web applications with pages that need to spawn/update more than 20,000 DOM elements all at once on the frontend (like rare SVG heavy applications) or applications that have a lot of client-side interactions that do not require the server, use Opal Ruby instead of JavaScript (potentially with an Opal framework or just Opal-JQuery, encapsulating any JavaScript logic that is performance-sensitive if needed). Ruby is after all much more readable, maintainable, and productive than all ES6+ incarnations and frameworks combined. And, you can build all your frontend abstractions (e.g. EmailFolder, EmailMessage, Contact, etc..) in pure Ruby, thus have frontend code that is as maintainable as server-side Rails code.
  4. When applying "JavaScript Sprinkles" using jQuery, make sure to divide the code by 4 levels of granularity depending on how much reuse you need of the code: Global applications.js, Controller-scoped resource.js per resource (e.g. users.js), Controller-Action scoped resource-action.js (e.g. user-show.js) per resource controller action, and reusable css-activated components (e.g. text_area_with_char_count.js). Some unskilled inexperienced developers try to excuse their over-engineering with SPA frameworks by claiming jQuery code is not well organized or reusable when in fact those developers are the ones who are not organized or applying sound software engineering principles in their use of jQuery, thus creating one large jQuery file and doing everything in it instead of dividing and conquering jQuery code using the four levels of gradularity mentioned above. Learn to master jQuery componentization! Contrary to popular misbelief, components are an old general software engineering concept, not something that some new SPA frameworks invented. Skilled software engineers have been using them since way before all SPA frameworks came out! You don't need an SPA framework to build components in JavaScript!
  5. Implement Wizards (Multi-Step Forms) as simply nested views of a resource's Edit/Update operation as per the Ultra Light Wizard Architectural Pattern, which is supported by the Wicked gem.
  6. Avoid using Dependency Injection containers as they are not necessary in Ruby due to its highly malleable dynamically typed nature. Dependency Injection containers come from Statically Typed languages like Java, and do not truly belong in Ruby as they are considered extreme over-engineering in it that multiplies code maintenance cost through unnecessary indirection.
  7. Use the right tool for the job! Avoid any sort of Static Typing in Rails application code whether using TypeScript, Elm, PureScript, or any of the Ruby static typing libraries like Sorbet and RBS. Ruby and JavaScript were never meant as statically typed languages, and there are direct benefits to be had in their dynamically typed nature that must not be forgone when building Ruby on Rails apps. People who use static typing in Ruby or JavaScript are like someone who is handed a fast bike, but decides to install 2 extra side wheels and ride it like a kid's tricycle very slowly. If you need static typing for few scenarios or parts of the application that have very high performance requirements, learn to be a Polyglot instead, and use Ruby in tandem with another statically typed programming language like Java (especially with JRuby), C, C++, Crystal, or Swift. Finally, if you are not seeking performance yet correctness through static typing, then write automated tests instead since you need them anyways whether the language is static or dynamic.
  8. Consider using Rails Engines and Rails Engine Patterns when you have components/concepts/objects that are repeated in multiple Rails applications, like in the case of multiple Rails applications sharing the same core domain model logic. Rails Engines offer the added benefit of slicing your test suite into multiple smaller test suites, and running a lot less tests much faster in the engines that change only or in the applications minus the code extracted to engines. That resolves a very big common problem with test suites taking too long to run in bigger codebases.
  9. Only build Rails API web services in the cases of needing to modularize and expose some of your domain models publicly to B2B or B2C clients or support SPAs (single page applications). Favor Rails Engines and avoid building Rails API web services (incorrectly named microservices by misguided programmers) in the situations of needing private reuse of domain models across Rails applications given that services increase complexity of setup/maintenance, latency, and security risks of web applications.
  10. Use CDNs for static assets in your Rails application to offload serving them from your web application, thus conserving your server CPU/Memory resources and relying on delivery-optimized CDNs, which serve static assets faster through location replication.
  11. Setup a performance monitoring service like New Relic when building an application that needs to serve many customers in order to recognize the weakest links in your application and optimize them.
  12. Add database indices to all columns that are used frequently in database queries by customers, but only once you confirm that the table has a very large number of rows and performance is not fast enough without the indices given there are trade-offs in adding them. Avoid adding too many indices or any to database tables that undergo very frequent inserts/updates (much more than reads).
  13. In some cases, you might need to denormalize/replicate database tables to optimize performance for both reads (e.g. reports) and writes (e.g. transactions).
  14. Use Rails caching at every level it is beneficial, but only when needed (web requests are taking more than 500ms to process) as caching complicates code maintainability.
  15. You neither want fat-model-skinny-controller nor skinny-model-fat-controller. Always aim at skinny-model-skinny-controller-skinny-view (yes, skinny everything, including the view too). What does that practically mean? Always refactor your code so that your classes/templates do not cross 200 lines of code. If a file grows too big whether a controller, a model, or a view, then divide and conquer it with understandable business domain model concepts (or Rails Partials/Helpers in the case of views), potentially following GoF Design Patterns and Domain Driven Design Patterns. You can sometimes relax the 200-lines-of-code restriction a bit, but certainly not more than 500 lines and if you have a file with 1000 lines, you're clearly in the unmaintainable code danger zone. Do not fall for abstraction libraries that try to re-invent Design Patterns under different names, applying them the wrong way (like Service-Object/Operation libraries re-inventing the Command Design Pattern or Role libraries re-inventing the Strategy Design Pattern while encouraging developers to add code for different roles under the same file instead of in separate strategy files).
  16. Avoid dividing the work between backend and frontend. All software engineers must be full-stack developers in order to be maximally effective at delivering value to customers. Developers who only know the backend or the frontend cannot possibly think of the entire value being delivered to customers end-to-end, so their work is always inferior. Every software engineer must be implementing features in vertical slices that cut through from the top layer of GUI (graphical user interface) to the bottom layer of the database.
  17. When requests take longer than 500ms, divy up some of the work to be handled asynchronously in the background. Have workers handle entire units of work cohesively. Avoid blindly divying up workers into too many fine-grained workers as that complicates maintainability and troubleshooting greatly (a common anti-pattern). Only break workers down if some of the work is reusable independently for other use-cases or part of the work must be allowed to fail and retried independently of other parts of the work (e.g. data transformation worker that needs to send an email at the end should have the email sending part be split into its own worker because a failure in emailing must not fail the already successful work of the data transformation).
  18. Background work can be handled either via queue workers (pull) or messaging publish/subscribe workers (push). The first case (queue workers) is recommended for simple tasks that only need to be handled by one type of worker, and once handled, they are consumed and no longer need further processing. The second case (publish/subscribe workers) is needed when a task must be handled by multiple types of workers (e.g. an app database ETL (Extract Transform Load) worker, a reporting database ETL worker, and an email worker), so all are notified of the work via a message, and each can handle differently. Note that publish/subscribe messaging can be simulated with queues by having a manager worker receive an initial task, and then break it into multiple sub-tasks to be handled by multiple types of queue workers. So, publish/subscribe messaging is only truly needed for very advanced cases that require many worker subscriptions, not just a few, or else it would be considered over-engineering.
  19. Avoid long running branches. They slow down productivity and severly increase the risk of causing bugs when merging back to master. Use abstract feature branches instead (aka feature flags) as per the abstract_feature_branch Ruby gem.

Software Design:

  1. Rails Helper Presenters of Model information
  2. There is no need for "View Component" libraries given that Rails already supports View Components via Rails Partials and well-named Rails Helper methods (remember that less is more with software engineering, so there is no need to re-invent the wheel with unnecessary view component libraries). And, don't fall for any blog posts that try to glorify the use of View Components in external libraries instead of Rails built-in idiomatic techniques. They all have holes in their arguments that just try to excuse the authors' lack of skills in Rails resulting in over-engineering by using unnecessary libraries.
  3. Modals (aka dialogs) are displayed instantly without making web requests (to avoid hindering user experience) by including as hidden Rails Partials in web pages that need them and then activating from front-end code when needed.
  4. Avoid repeated conditionals by refactoring to the Strategy Design Pattern (or in some cases State Design Pattern), manually or using gems like Strategic.
  5. Use the State Design Pattern when having state-dependent logic, keeping the code close to the business domain, instead of the State Machine approach, which is a low level Computer Science concept that distances the code from the business domain, thus not recommended except in the theoretical computing field.
  6. Avoid all misguided programming techniques that impractically require immutability, over-emphasize mathematical functional computations, and neglect natural Object-Oriented abstractions (e.g. having an Order object represent a real world Order that encapsulates all the included products and quantities) as they result in code that is distanced from the business domain model (thus losing half the battle already in meeting the demands of customers intuitively) and is very difficult to maintain except by the one overly-proud self-congratulatory person who wrote it (who won't be able to maintain it once some time has passed too as they hop unto the next buzzword hyped misguided programming technique to rewrite the bad code they could not comprehend anymore). By the way, there is nothing wrong with mathematical functional style of programming. FP is where OOP came from as an advanced application of it whereby data is smart data that knows its own operations. Regressing to dumb data on the other hand when it is inappropriate for the problem is like ditching airplanes and getting back into riding horse carriage. Furthermore, functional style is only appropriate for processing highly mathematical algorithms whereby the domain model is math. Trying to force a math domain model on a business model that is not mathematical is violating Domain Driven Design practices and is a mistake that unskilled uneducated programmers make because they blindly follow bad advice from the Internet instead of thinking for themselves, often comically repeating arguments from the Internet verbatim word for word without truly understanding anything they say.
  7. Cover sensitive parts of your Ruby on Rails app with automated tests. But, avoid silly policies mandating 100% code coverage or 100% TDD. Remember that serving the customer is always the highest goal, and sometimes that is done by not wasting time on unnecessary tests (like when quickly implementing a CRM through a Rails Engine).
  8. Apply the Value Object pattern when objects are identified by their attributes without ever needing an update (e.g. a zip code object identified as 60611). It simplifies the code, avoids the need for a disk-based database table or joins thus improving querying performance around those objects, and enables simpler caching techniques.
  9. Use ActiveModel to model objects that do not require persistence instead of ActiveRecord. Some objects represented by Rails resources do not truly need ActiveRecord since they are transient, meaning used temporarily in a transaction to do things like send an email or perhaps produce multiple ActiveRecord models that are stored in the database. Avoid any libraries that provide typed struct support as they are not Ruby-idiomatic and if that is ever needed (for performance optimization only), you could use a statically typed language that is more suitable for that need.
  10. Divide and conquer your Rails Routes when they get out of hand!
  11. Avoid overuse of Active Record Callbacks by dividing and extracting their logic into meaningful Observers and mixin modules instead, considering the Wisper gem.
  12. Avoid fashionable "monad" libraries! They are extreme over-engineering and inferior to Ruby-idiomatic techniques!

Bad example using dry-monad:

def find_user(user_id)
  user = User.find_by(id: user_id)

  if user
    Success(user)
  else
    Failure(:user_not_found)
  end
end

def find_address(address_id)
  address = Address.find_by(id: address_id)

  if address
    Success(address)
  else
    Failure(:address_not_found)
  end
end

user = yield find_user(params[:user_id])
address = yield find_address(params[:address_id])
Maybe(user.update(address_id: address.id))

Good example re-written using Ruby-idiomatic techniques:

user = User.find_by(id: user_id)
address = Address.find_by(id: address_id)
address && user&.update(address_id: address.id)

See, how it is much shorter and simpler, let alone it does not require codebase newcomers to learn a new library that is unnecessary (just imagine the size of a big codebase that has 7x the code and maintenance expense just to use that library)! Popularity is not a measure of quality. Millions of people use PHP for example, but that does not make it any good. That just means millions of people are making the wrong decision. I realize that hypers of monads like to claim big risks about the dangers of working with nil values and like to cite famous quotes like "null is the billion dollar mistake", but such sayings only make for fancy buzz and hype while in practical real-world scenarios, the risks never truly materialize assuming good software engineering habits like automated testing and good QA. Usually, people who find working with nil a very big problem are either unskilled or uneducated. They construct extremely elaborate and complicated techniques to get around their own shortcomings instead of actually developing their skills and truly overcoming their shortcomings (using React is another example of that sort of crutch for the unskilled and uneducated). It's like a bicycle learner who failed to ride his bike and attached 2 extra side wheels instead of developing his skills at riding two wheels only. Be a pragmatic skilled software engineer instead!


Hit me up in Issues if you have any questions about best practices you do not understand or think there are mistakes in the best practices.

Feel free to submit Pull Requests if you know of best practices that are missing and would like to add to this best practice list.