Overuse of Rails engines


All of us would have used the well-known Devise gem for authentication in Rails but we can also say it’s an engine. So, simply put a Rails engine is the miniature edition of a Rails application. It comes with its own app folder which includes controllers, models, etc The only difference is, that an engine won’t work on its own so, to make it work we need to inject it into the main Rails application.

Rails engines were introduced to deal with bloated architectures by creating modular and reusable components but, everything is a curse if it is overused.

Rails engines can be a powerful asset if implemented using a well-thought design otherwise it could lead to various problems.

Leaking of app information to engines

An application could leak information to engines and engines could use the information to do unexpected things.

Let’s say an engine expects an author_name from the main app using which it tries to find and associate the user to the Post model and because the database is shared, the engine is creating a User if not found, without the knowledge of the main app and this could lead to serious issues.

  module Engine
    class Post < ActiveRecord::Base
      attr_accessor :author_name
      belongs_to :author, class_name: "User"

      before_validation :set_author

      private
        def set_author
            self.author = User.find_or_create_by(name: author_name)
        end
    end
  end  

Here, we can also notice that the Engine has full control over the database and can perform any of the operations on the User.

Also, for example, suppose our main app has a posts_controller and a route helper posts_path and if our engine tries to use a helper posts_path then it can use it without an error so, this is how things can leak through boundaries between main app and engines.

Don’t use unless code shared across code bases

There is no point to use Engines unless we are planning to share them between the projects. Engines add little meaningful encapsulations to the architecture and are just a way to mix one Rails project into another. This is useful if we are planning to release the next Devise as a gem. But also, having a gem as a critical part of our project is also a royal pain, because it is by definition harder to change than the rest of the project so, we can always try with Ruby modules or libs first.

Harder to test engine code

Engines make TDD almost impossible. The engine needs it’s own full fake Rails app and database to live in to run any tests, which is an overhead to impose on any project’s development team.

To write engine tests that depend on main app models we need to generate main app models inside the test/dummy directory defined in the engine and need to mount the engine on this dummy app as mentioned in docs.

Less cohesion and coupled to app code

The idea of cohesion is to have applications that can evolve independently which means if we change one component then we don’t need to change another but it is not true in the case of engines, for example, our blog engine finds a user by name and what if we rename a name attribute to username, we will need to update the engine code to use this new attribute.

  module Engine
    class Post < ActiveRecord::Base
      attr_accessor :author_name
      belongs_to :author, class_name: "User"

      before_validation :set_author

      private
        def set_author
            self.author = User.find_or_create_by(name: author_name)
        end
    end
  end  

These issues occur because we generally don’t have a Bounded Context defined between our application and engines.

Also, the engine and app is using the same database. By creating an interface separate from our data storage, we smooth the road toward having a fully decoupled service with its own data store. Furthermore, we explicitly define the interactions with the data and behavior within the engine, rather than using the database as an interface.

Unnecessary code and dependencies

There is unnecessary and redundant code that gets checked into VCS because the engine has its own app directory along with the .gemspec file. So with the main app, we also need to take care of the engines to clean up the unnecessary code and dependencies.

Multiple configs and gemfiles.

While using engines we need to deal with creating separate configurations for the main app and engines and that could result in some subtle serious issues.

Engines define dependencies with a .gemspec, not a Gemfile, meaning our gems need to be available on a gem server like RubyGems and can’t be loaded directly from GitHub. we can always use a gem locally, but that’s a hassle. we also need to be careful to use similar versions of dependencies between engines and the main app. For example, if our main app is using a Devise 4.0 and the engine is using Devise 3.0 with different configurations then the engine will not work as expected because the engine devise will load the configuration from the main app initializer, not from the engine’s initializer.

Join Our Newsletter