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.
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.
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.