Modular Monolith
Context and Problem Statement
Large applications need clear boundaries to avoid turning into a ball of mud. However, Ruby does not provide a good solution to enforcing boundaries between code.
Resources:
- Deconstructing the Monolith: Designing Software that Maximizes Developer Productivity
- Under Deconstruction: The State of Shopify’s Monolith
- Modular Monolith: Integration Styles
Proposed solution
Rebuild the application in a modular way:
- Easier to understand, less cognitive load: It’s easier to see where the bounded contexts are drawn.
- Easier and faster to test: You only need to test the engines affected and the ones up your dependency chain.
- Less coupling, easier to make changes: If you have your boundaries correctly defined it will be easier to swap out components.
- Less merge conflicts: Because a change is more isolated to a specific domain the changes will be confined to that module only.
Considered Options
There are two different problems to solve with complementary tools:
- Modules loading: how these modules are loaded by the Rails application
- Modules boundaries: how to set modules dependencies, strict boundaries and a clear public/private APIs
Modules loading
- Split
app
into multiple directories added to Rails autoload path - Create Rails Engines
Modules boundaries
Decision Outcome
Chosen option: Rails Engine + Packs rails
Module loading
Adding folders to the autoload path is simpler but Rails Engines are the native solution to create isolated modules, additionally it provides the following features:
- module gemspec (isolate gems)
- module isolation: isolate controllers, models, etc
Modules boundaries
Gems are the native Ruby tool to package libraries but they require extra tooling and deployment. This would force us to generate a monorepo/multi-repo for all of them and updating our CI tools, worsening development experience. Packwerk is a mature tool from Shopify, also creators of the Zeitwerk Ruby class loader built for monolithic applications. It can be used with any Ruby project and can be easily integrated to a Rails project. Packs extend Zeitwerk and defines a convention for modules and solves the autoloding through Engines and RSpec integration. Additionally it has the following checks:
- A privacy checker that ensures other packages are using your package's public API
- A visibility checker that allows packages to be private except to an explicit group of other packages.
- An experimental architecture checker that allows packages to specify their "layer" and requires that each layer only communicate with layers below it.