Skip to main content

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:

Proposed solution

Rebuild the application in a modular way:

  1. Easier to understand, less cognitive load: It’s easier to see where the bounded contexts are drawn.
  2. Easier and faster to test: You only need to test the engines affected and the ones up your dependency chain.
  3. Less coupling, easier to make changes: If you have your boundaries correctly defined it will be easier to swap out components.
  4. 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:

  1. Modules loading: how these modules are loaded by the Rails application
  2. Modules boundaries: how to set modules dependencies, strict boundaries and a clear public/private APIs

Modules loading

  1. Split app into multiple directories added to Rails autoload path
  2. Create Rails Engines

Modules boundaries

  1. Multiple gems. (example)
  2. Packwerk (example)
  3. Packs Packwerk + extensions

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.