Is a Ruby gem to help separate concerns inside a Rails monolith.
https://github.com/Shopify/packwerk
Treat data modeling for packages like data modeling for network-isolated services:
hold foreign IDs referencing models across package boundaries, but do not hold strictly enforced foreign database keys and do not use ORM associations that would hide the separation of concerns.
Following the upper advise would be quite a change for our models. But less dependent: :destroy | :nullify | nil
decisions could actually make our lifes easier and our data more consistent (or just more fault tolerant). And not to forget that Packwerk allows to start small and increase its checks file by file.
Move code with high cohesion into packages and then enforce boundaries by defining permissions and restrictions and a failing linter on the CI.
A package should contain an app/ folder with all the typical and custom subfolders like controllers/, models/, services/ and so on and a spec/ folder with all the model/, request/ and other specs relevant to this packages.
Let's keep the following folders on the root level of our Rails app:
- lib/ folder with all our app specific code, that is neither business logic nor managed by Rails
- /db folder, as we will keep using the same database (possibly we'll introduce namespaces to tables)
- /config folder as it contains general configuration for all the app
Creating a package just means to:
- copy the code into a new folder app/packages/package-name/
- adding a packwerk.yml file to this folder, configuring the boundaries
Every bullet point with items under it, should be a package - this means nesting packages.
-
organization (for now propably just keep code in the "base" / "main" app package)
- company
- project
- user
-
branch
- routing
- route management
- internal
- traveling-salesman
- mapbox
- wdl
- sync-services
- route generation
- routing
-
industry
- tickets
- order
- billing
- plant
- site
- area
- zone
- tickets
-
resources
- container
- device (or does this belong into routing?)
- location (or use more specific name like: garage / dumping_station)
- vehicle
-
emptying planning
- emptying templates (or do they belong to container or routing?)
- sensors and fill_level data (or do they belong to container or routing?)
Start with just one package - or only a part of a package - copy all the files belonging together (high cohesion) into a new package (==subfolder) and add the packwerk config files to this folder.
Don't update the code or naming just yet - only define the boundaries (which files / packages are allowed to access anything in this package && which files / packages can be access from inside this package) and generate an allow-list by the violations that will be generated.
Only now start introducing code changes to establish clear boundaries (like an API) between packages.
Enforce the boundaries by defining them more strictly, reduce the exceptions and let the CI fail in case of violations. This should entail that no package can directly access a model class in a another package or receive an instance of a model. Use serialized objects like in an API and use service / api classes if you need to trigger a CRUD operation.
I would also recommend to move all the code into a package namespace (including its database tables), but this might be easier todo, once most calls are done inside a package with clearly defined exceptions.
- Shopify blog:
- Very clear and simple step-by-step guide
- Good explanation why and how to break a monolith into packages
- Step-by-step guide for packwerk including graphwerk visualization of dependencies:
- Examplatory Packwerk usage at a company:
- Similar concept, using Rubocop to enforce Rails Engine boundaries:
- General post about the "Modular Monolith"
- General post about enforcing boundaries in a Modular Monolith
As a bonus a book in the making: Gradual modularization for Ruby and Rails: