rails architecture engines

Component Based Architecture

A component contains reusable business functionality that can be referenced and used by other components. Components can associate with one another by setting an explicit dependency. The collection of components and their dependencies can be diagrammed in a directed acyclic graph, a graph with no cycles. Meaning, no circular dependencies. For those of you experienced with React JS, you are already familiar with component based architecture.

Modular Approach in Rails

Applying this pattern in Rails can be done with mountable engines. Your main application, often referred to as your host, will contain a folder we call engines. You can name the folder what ever you want. components is another good name. I prefer engines as we typically also generate a gems folder to differentiate between the two but unimportant for the scope of this post.

Nested within the engines folder will live these mountable engines we call components. It is made available to the host application just like any other gem, via the gemfile.

Creating an Engine

Considering a real world example for an app that needs to support generating reports: Lets create a Reports engine.

First step is to create the mountable engine. We do so by using the rails plugin new generator. The generator accepts a variety of options that you may want to use. See the full list by running rails plugin --help.

root $ rails plugin new engines/reports --mountable -d postgresql

The generator should have created a mini application in your engines/reports folder. It also should have modified your host application’s gemfile by appending:

gem 'reports', path: 'engines/reports'

I usually remove that line and add the engines dynamically like so:

Dir.glob(File.expand_path('../engines/*', __FILE__)).each do |path|
  gem File.basename(path), path: path
end

Go ahead and try to bundle from your host.

root $ bundle install

You have one or more invalid gemspecs that need to be fixed.
The gemspec is not valid. Please fix this gemspec.
The validation error was '"FIXME" or "TODO" is not a description'

Even though we are creating a local gem we still need to complete the TODO’s in the gemspec. Modify host/engines/reports/reports.gemspec and ensure you can bundle from your host directory.

Create Our First Model

Now that we have an engine, we can go ahead create a model. If you recall, we used the --mountable option when creating the engine. That isolated the engine and name-spaced everything with Reports, including the database tables.

Navigate to the reports directory and create a report model.

root $ cd ./engines/reports
root/engines/reports $ rails g model Report name data:text

Our new migration file exists within our reports engine db/migrate folder. Think of this migration file as the source of truth. The migration files are managed within the engine although they will also need to exist in the host application. We must copy our migration over to the host application. This is done by the railties install migration command.

root $ rails railties:install:migrations

Copied migration 20190811131059_create_reports_reports.reports.rb from reports

You should notice a few things:

  • The report table is name-spaced by reports. Table name is reports_report
  • The original CreateReportsReports migration file exists in the reports engine
  • A copy of CreateReportsReports lives in the host application
  • The original and copied migration file versions (time-stamps) are different

It is safe to rerun the railties install migration command. It will only copy over new migrations.

From you host, migrate the database.

root $ rails db:migrate

Business Logic

It is time to add some business logic to support generating a report. Add a file named generator.rb.

# root/engines/reports/lib/reports/generator.rb

module Reports
  class Generator
    def self.run(name:)

      # Some meaningful code here

      Report.create(name: name)
    end
  end
end

And modify our reports.rb file to require the generator.

require 'reports/engine'
require 'reports/generator'
...

Now, you can generate reports

Reports::Generator.run(name: 'Sample Report')

Future Steps

This post was written to introduce Component Based Architecture with Rails and walk you through creating your first engine. I encourage you to read more to learn about the benefits and drawbacks. Understanding both is important before adapting any architecture and using it in production applications.

I have used this technique with great success and believe many code bases would benefit from this modular monolith.

Useful Resources

If you are interested in learning more about applying Component Based Architecture to your Rails application then you might find some of the following links useful.


Found this useful? Know how it can be improved? Get in touch and share your thoughts at blog@amrani.io