Referenced article is among the best I’ve read in the past year. While I don’t agree with everything stated there, the ideas described are super awesome (this is when I found out there is a limit on a number of claps you can give in medium). I’ve tried to apply them in my pet project that is built with classic Rails structure - while extraction wasn’t easy the result was definitely worth it. Here I’ve extracted a gem and an engine from the project into a brand new Rails application and it was painless, moreover this extraction improved the design of the engine. I will cover important pieces of the setup below, but for TLDR people here is a Github Repo, check the seed.rb file for credentials to use.

Gem Overview

The gem allows you to fetch event data from a Google Calendar

separation of dependencies

Code in the local gem has it’s own dependencies but does not rely on a main app, these dependencies were moved from the parent app’s Gemfile into a gem’s *.gemspec

 # gems/google_calendar/google_calendar.gemspec

 32   spec.add_dependency 'activemodel'
 33   spec.add_dependency 'google-api-client', '~> 0.11'
 34   spec.add_dependency 'ice_cube'
 35   # Development
 36   spec.add_development_dependency 'pry-byebug'
 37   spec.add_development_dependency 'simplecov'
 38   spec.add_development_dependency 'rake'
 39   spec.add_development_dependency 'rspec'
 40   spec.add_development_dependency 'factory_bot'

and are loaded in initializer

  # gems/google_calendar/lib/google_calendar.rb
  1 require 'google/apis/calendar_v3'
  2 require 'google/api_client/client_secrets'
  4 require 'google_calendar/version'
  5 require 'google_calendar/connection'
  6 require 'google_calendar/event'

separation of tests

All Unit tests for the gem were moved to a gem’s folder, they can be executed independently and in isolation - navigate to a gems folder and run bundle exec rspec spec/. In order to make tests run you will need to do a manual setup in helper file

  # gems/google_calendar/spec/spec_helper.rb
  1 require 'simplecov'
  2 SimpleCov.start
  4 require 'google_calendar'
  5 require 'factory_bot'
  6 require 'factories/event_factories'

Engine Overview

Engine provides authentication using authlogic gem

separation of dependencies

Same pattern as in gems, all dependencies live in engines *.gemspec and do not pollute parent app’s Gemfile.

 # domains/customers/customers.gemspec
 19   s.add_dependency 'authlogic'
 20   s.add_dependency 'best_in_place', '~> 3.0.1'
 21   s.add_dependency 'draper'
 22   s.add_dependency 'google_calendar'
 23   s.add_dependency 'haml'
 24   s.add_dependency 'rails'
 26   s.add_development_dependency 'rspec-rails'
 27   s.add_development_dependency 'factory_bot'
 28   s.add_development_dependency 'shoulda-matchers'
 29   s.add_development_dependency 'pry-byebug'
 30   s.add_development_dependency 'sqlite3'

and are loaded in the initializer

  # domains/customers/lib/customers.rb
  1 require 'active_model/railtie'
  2 require 'active_record/railtie'
  3 require 'customers/engine'
  4 require 'haml'
  5 require 'best_in_place'
  6 require 'authlogic'

engine also depends on a local google_calendar gem, it is loaded directly in the Gemfile

  # domains/customers/Gemfile
  1 source ''
  3 gem 'google_calendar', path: '../../gems/google_calendar'

separation of tests

All Unit tests for the engine were moved to a engine’s folder, they can be executed independently and in isolation - navigate to a engine’s dummy application folder domains/customers/spec/dummy/ and run bundle exec rspec spec/ In order to make tests run you will need to do manual setup of the environment in the helper file

  # domains/customers/spec/dummy/spec/rails_helper.rb
  1 # Configure Rails Environment
  2 ENV['RAILS_ENV'] = 'test'
  3 require File.expand_path("../../config/environment.rb", __FILE__)
  4 # TOFIX ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../test/dummy/db/migrate", __FILE__)]
  5 ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__)
  7 require 'rspec/rails'
  8 # Add additional requires below this line. Rails is not loaded until this point!
  9 require 'spec_helper'
 10 require 'authlogic'
 11 require 'authlogic/test_case'
 12 require 'factory_bot'
 13 require 'shoulda-matchers'
 14 require 'pry'
 16 FactoryBot.factories.clear
 17 FactoryBot.definition_file_paths = %W(spec/factories)
 18 FactoryBot.reload
 19 Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
 21 RSpec.configure do |config|
 22   config.include Authlogic::TestCase
 23   config.include FactoryBot::Syntax::Methods
 24   config.include Shoulda::Matchers::ActiveModel, type: :model
 25   config.include Shoulda::Matchers::ActiveRecord, type: :model
 27   config.filter_rails_from_backtrace!
 28 end

separation of migrations

I believe migration files should not be copied to a parent application, required configuration is specified in engines initializer

  # domains/customers/lib/customers/engine.rb
  1 module Customers
  2   class Engine < ::Rails::Engine
  3     isolate_namespace Customers
  5     initializer :append_migrations do |app|
  6       # Migrations
  7       config.paths['db/migrate'].expanded.each do |expanded_path|
  8         app.config.paths['db/migrate'] << expanded_path
  9       end
 12     end
 13   end
 14 end

separation of translations

Translations for views from an engine also live in an engine, configuration is specified in engines initializer

  1 module Customers
  2   class Engine < ::Rails::Engine
  3     isolate_namespace Customers
  5     initializer :append_migrations do |app|
 10       # Translations
 11       config.i18n.load_path += Dir["#{config.root}/config/locales/**/*.yml"]
 12     end
 13   end
 14 end

Enabling authentication in the main application

Authentication was extracted to be a controllers concern so you need to add this concern to a controller

  # app/controllers/application_controller.rb
  1 class ApplicationController < ActionController::Base
  2   include Customers::Authorization
  3 end

Testing Engine/Gem Integration into a main application

I believe that tests located in engines/gems should be unit tests - they should run fast and stub any external dependencies. It doesn’t make sense to me to test integration outside of the main applications - System Tests are great tool to do this job. A basic example

# test/system/login_test.rb
  1 require 'application_system_test_case'
  2 require File.join(Rails.root.to_s, 'test', 'support', 'pages', 'accounts', 'login')
  4 class UsersTest < ApplicationSystemTestCase
  6   def test_login_is_functional
  7     load "#{Rails.root}/db/seeds.rb"
  8 self, url: customers.login_url ).instance_eval do
  9       visit
 10       # Validate content
 11       password_present?
 12       login_present?
 13       submit_present?
 14       # Log in
 15       login.set( )
 16       password.set( 'Test1234' )
 18       assert_text( 'Accounts' )
 19     end
 20   ensure
 22   end
 23 end


Extracting a gem was quite easy, extracting an engine was a bit of work. Advantages of the Modular Monolith over the classic app:

  1. Separation of code - dramatically improved application design
  2. Separation of dependencies - keeps main application cleaner
  3. Separation of tests - each unit has it’s own suite that is fast and can be run independently


Food for thought

  1. How to handle shared layouts?
  2. How to handle database tables shared between engines?
  3. Should Gemfile.lock from engines/gems exist in the Git repository?