Taming Rails Apps with Engines

|   May 21, 2013

As the engineering team was planning for the recent Socialcast release we realized we were going to be adding a couple of significant new functional areas that each had their own models, views, assets, etc.  We were concerned that this could result in a sprawling code base that was hard to learn and maintain.

We came up with two ways to solve this in our Ruby on Rails application: either we could break it down into multiple applications and use API calls or we could isolate the functional areas using Rails Engines.  We knew that we wanted a seamless, integrated experience that was easy to maintain so we decided to move forward with the Engines approach.  It provided a lot of the benefits that we were looking for and a couple of surprise benefits that we wanted to share.

Implementation

Typically, Engines are used to distribute a mini-app as a ruby gem which can then be mounted in an existing application.   We wanted to do something similar, treating Projects as a mini-app, but without the hurdle of packaging a gem.  We created an “engines” directory in the top level of our Rails app and put each Engine in a subdirectory.

Our first Engine was “projects” so I’ll walk you through the steps to add it.  We’re using Bundler so in order to pick up the Engines we add this to our Gemfile:

gemspec :path => 'engines/projects'

Inside the “projects” directory we have our mini-app with a basic gemspec (to keep bundler happy) as well as the normal Rails directories (app, config, lib, spec, etc).  If you’d like to learn more about how to build Engines there is an excellent RailsGuide and a RailsCast.

Aside from the plumbing the things we put in the engine were:

  • models
  • controllers
  • assets (css, javascript, images)
  • routes
  • i18n keys (config/locales/en.yml)
  • tests

Benefits

Once we had a working Engine we wanted to evaluate whether this had provided enough benefit to justify the complexity.  The primary goal was to keep the app maintainable and easy to learn.

We felt that we had succeeded in a number of areas:

  • It’s easy to understand the Projects data model because all of the model files are together.
  • The “projects” folder is a natural spot for a README.  This is good place to provide an overview of the functionality and data model for new developers.
  • The connection between the models, controllers and assets makes it easy to see how things fit together.

Our existing team also found several benefits in using Engines:

  • You can run the Projects tests independently when making changes to Projects.
  • It’s natural for the Projects area to have its own CSS and Javascript bundles that extend the core functionality.  This keeps our core Javascript file from growing too big and also makes it less likely for a Projects Javascript / CSS bug to affect the rest of the app.
  • It is much easier to identify unused images, scripts, etc. when they are grouped together with the views and CSS files that reference them.

Challenges

Aside from a little more depth in our directory structure there were a couple of things that were challenging about this approach.

The main problem was how to extend core models without introducing a lot of Projects-specific code.  We ultimately decided to use Concerns which we then mixed into the core models.  This worked really well for some things – adding associations, validations, callbacks and methods.  However in some cases the most straight-forward implementation would have required a lot of “if” checks in the core code which we wanted to avoid.  We decided to avoid that and add custom extension points for those cases.  Later we found that we often found ways to reuse the same extension points for other modules of the system.

As we added more engines we also wanted to make it easier to add new engines.  In a few places we loop over the installed engines to avoid having to add an entry to a central file for each entry.  For example, here’s how we set up our Gemfile:

Dir.glob(File.dirname(__FILE__) + '/engines/*/*.gemspec').each do |engine_gemspec|
gemspec :path => File.dirname(engine_gemspec)
end

Conclusion

The engineering team has really liked the Engine changes and I’ve gotten good feedback from our recent new hires.  We have gone back and extracted some of our previous components into engines and found that it’s a great way to isolate independent modules of the system to keep things understandable and maintainable.  We’re currently up to nine engines and are also using them to build some of the exciting new features that are coming next.

Comments

  • Thanks for sharing, great insight on Rails Engines !

    A few questions :
    * do you have one git repo per engine ? or everything in the core-app repo ?
    * what do you mean by “custom extension point” ?
    * did you create totally-independent engines ? or do you use resources from the core-app in the engines ? (assets, models, helpers…)

    Commented on May 23, 2013 at 4:39 am
    • * We include everything in one repo. For us I don’t see a big advantage in separate repos and it makes it simple to test and release everything together.
      * By “custom extension points” I meant that we added hooks in our core models that the engines use to contribute to the core behavior. Often these are implemented as cattr_accessor Arrays on the models which the engines append to. An example of this would be an engine that wants to change the email notification sent when a new message is posted.
      * The engines are not independent. Our intent is to have one-way dependencies, so the engines such as Projects would depend on the core app but the core app would not depend on Projects. There is no enforcement on this however, we considered running the unit tests with the engines disabled to verify that the core was not dependent on them but haven’t taken that step.

      Commented on June 3, 2013 at 6:24 am

Leave a comment

Your email address will not be published. Required fields are marked *

Connect with Facebook

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Sign up to receive email communications regarding events, webinars, and product news.

What is Socialcast?

Socialcast by VMware (NYSE: VMW) is a social network for business uniting people, information, and applications with its real-time enterprise activity stream engine. Behind the firewall or in the cloud, Socialcast enables instant collaboration in a secure environment. Socialcast is headquartered in San Francisco, California. www.socialcast.com