rails architecture engines devise authentication rmm

In part one, we created the ConnectBy engine and installed Devise. The source code is on github.

For this third part we will demonstrate how we can configure a rails engine in our modular monolith application.

Controllers

I’ll configure my engine to use my host’s application controller. Then all of our engines can 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.configure do |config|
  config.application_controller = "ApplicationController"
end

The engine will then need to handle the configure block.

# engines/connect_by/lib/connect_by/engine.rb

module ConnectBy
  class << self
    def configure
      yield Engine.config
    end
  end
  ...
end

Update the engine’s application controller.

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

module ConnectBy
  class ApplicationController < Engine.config.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 < Engine.config.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 < Engine.config.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 error is raised.

Contracts are in place to set expectations. We can go further but we have found this sufficient.

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.

Takeway

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