In part two, we added rspec and factory bot to our rails engine.

For this third part, I’ll demonstrate how to configure a rails engine in a modular monolith application.

Source Code

Controllers

I’ll configure my engine to use my host’s application controller. Then our engines share common behavior like :current_user or some_useful_method. We do this without compromise and still respect our dependency boundaries.

Let’s add a connect_by initializer:

# config/initializers/connect_by.rb

ConnectBy.application_controller = "ApplicationController"
# engines/connect_by/lib/connect_by/engine.rb

module ConnectBy
  mattr_accessor :application_controller

  ...
end

Update the engine’s application controller.

# engines/connect_by/app/controllers/connect_by/application_controller.rb

module ConnectBy
  class ApplicationController < ConnectBy.application_controller.constantize

Add behavior from the host so the engine can use it.

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protected
    def some_useful_method
    end
end

some_useful_method is now hooked into connect_by. Although it works, we can do a lot better.

I want it to be completely apparent that connect_by’s application controller is dependent on some_useful_method. We can accomplish this with a contract.

Raise an error if connect_by’s application controller does not have it defined.

# engines/connect_by/app/controllers/connect_by/application_controller.rb

module ConnectBy
  class ApplicationController < ConnectBy.application_controller.constantize
    raise "Must implement some_useful_method" unless instance_methods.include?(:some_useful_method)

Another improvement is how we organize this functionality. Refactor it using a controller concern.

# app/controllers/concerns/connect_by/controller_behavior.rb

module ConnectBy
  module ControllerBehavior
    protected
      def some_useful_method
      end

Refactor the host application controller to include the concern.

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include ConnectBy::ControllerBehavior
end

And then update our contract:

# engines/connect_by/app/controllers/connect_by/application_controller.rb

module ConnectBy
  class ApplicationController < ConnectBy.application_controller.constantize
    raise "Must include ConnectBy::ControllerBehavior" unless self < ConnectBy::ControllerBehavior

    raise "Must implement some_useful_method" unless instance_methods.include?(:some_useful_method)

I’ll test to see if it worked by navigating to /a/users/sign_in in the browser.

Try removing some_useful_method from the application controller and confirm the raised error.

Contracts are in place to set expectations.

Many programming languages have native support for contracts. For instance, Eiffel has DBC (Design by Contract) built-in. Also, the team at Blue Bottle Coffee shared a repo and created a DSL for contracts in rails.

Takeaway

We configured our engine to use the host’s application controller by adding an initializer and updating our engine. We did so without breaking our dependency tree and keeping our monolith modular.

The source code for this part is on Github.

Useful Resources

I have compiled a list of useful resources for rails engines and the modular monolith architecture.

Scale With Rails Engines

Need help scaling your rails application with a modular monolith? Talk to us