For this third part we will demonstrate how we can configure a rails engine in our modular monolith application.
I’ll configure my engine to use my host’s application controller. Then all of our engines can share common behavior like
some_useful_method. We do this without compromise and still respect our dependency boundaries.
Let’s add a
# config/initializers/connect_by.rb ConnectBy.configure do |config| config.application_controller = "ApplicationController" end
The engine will then need to handle the
# 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.
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.
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.
- Part 1 - Create Your First Rails Engine
- Part 2 - Test Your First Rails Engine
- Part 3 - Configure Your First Rails Engine
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