After presenting my Taming Monoliths without Microservices talk at Rubyhack last week, several folks came up for a discussion afterward. They posed the following question, “For pulling apart a monolith, should we use Rails Engines? If we should use them, are there any gotchas to keep in mind?”

I didn’t have an immediate answer, but we were able to workshop 3 specific tips for safe engines usage.

What are Engines?

From the docs:

Engines can be considered miniature applications that provide functionality to their host applications. A Rails application is actually just a ‘supercharged’ engine, with Rails::Application class inheriting a lot of its behavior from Rails::Engine.

In short, Rails Engines give us a powerful primitive for slowly breaking apart our monolithic Rails applications before considering the jump to a multi-repo architecture. As with many things in the Rails ecosystem, this can be a sharp knife. Engines can be a powerful asset in your toolbelt, or something that you regret using in the first place.

3 Best Practices

The 3 tips here are the following:

  1. A Rails Engine should not expose details of its data store or ORM to the host application. Create an interface that returns value objects.
  2. A Rails Engine should not rely on any behavior in the host application. It should not “reach up” and create a circular dependency.
  3. A Rails Engine should have its own test suite that is able to run in isolation. You may have a integration test suite to make sure the application as a whole functions when plugged together.

1. Don’t Expose Datastore Representations

Usually we reach for something as heavy as an engine because we want to create a Bounded Context within our application. This is likely a signficiant piece of the application, consisting of multiple models and several distinct business processes. We do not extract things into an engine until we have a strong understanding of the domain, and we have confidence that the data and behavior we extract belongs together.

By creating an interface separate from our data storage, we smooth the road toward having a fully decoupled service with its own data store. (Think: a microservice running on its own set of hosts with its own database.) Furthermore, we explicitly define the interactions with the data and behavior within the engine, rather than using the database as an interface.

We use value objects (think immutable POROs) to communicate in to and out of the Rails engine to ensure we never couple ourselves to the hose application through the passing of rich, ActiveRecord objects.

2. No Circular Dependencies

The depedency flow is already set by the structure of the application: the host knows about and depends on the engine. We should take great care to make sure the engine is self-sufficient and does not know about the host.

By doing so, we ensure that we do not create a circular dependency within our application. We want to avoid creating circular dependencies so that the different components of our application can move independently. We’ll be more confident in our changes, and we’ll be able to ship customer value faster.

If we do need to send information from the engine to the host application, we can work around this through dependency injection on initialization to define an event handler for callbacks from the engine itself. Here’s how we might do that:

# config/initializers/boot_engine.rb

MyEngine.event_handler = HostApplicationEventHandler

For accessing information within the engine, we can use its defined APIs as outlined in (1).

3. Self-Sufficient Test Suite

We extract engines, services, microservices, and more to get back to having autonomy and having a fast test suite. Being in charge of your own destiny with a fast test suite at your side makes for a great project and a healthy team. The test suites of our Rails Engines should be able to be run on their own, without needing behavior or data from the host application.

Conclusion

The conversations and talks at Rubyhack were productive! I recommend going next year if you get the chance. If you have any follow-up thoughts on Rails Engine best practices, please don’t hesitate to email me.