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.
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.
- Part 1 - Create Your First Rails Engine
- Part 2 - Test Your First Rails Engine
- Part 3 - Configure Your First Rails Engine
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