It might be just me, but it seems like Rails Engines is missing in the discussion. How does Packwerk compare to that approach?
I think the core issue they are addressing is not whether components of an application with separate concerns be isolated for configuration, convenience, development or testing. That can certainly be done with Engines, or even with a small team and a strong commitment to internal organization/conventions.
This seems to be “solving” something that lies at a much deeper level, and which those two approaches would leave unsolved (or tool-assisted variations thereof, as discussed in the post). Namely, that Ruby constant resolution allows for you to reference any constant, anywhere, for any purpose, after it is loaded (from anywhere else even if you didn’t know it!). It is the difference between what happens (in all every other module/file/context) after a call to Ruby’s “require” versus after a call to Python’s “import”.
One could take a self-contained grouping of four models, two controllers, and a few interactors, make them into an Engine, and document that “This mini-domain should be treated as an external API, and only accessed as such!” However, there is nothing stopping someone in the primary application from just referencing one of those models by name deep in some other application code, completely invalidating that attempt at isolation.
This kind of problem can be enforced with some discipline (docs, code review, or otherwise), but the likelihood that this gets violated is one that scales with team size, codebase size, and rate of change within a Ruby project. Just as noteworthy: these are all efforts that would not be required in other ecosystems, where there is a more restrictive answer to the question “Which constants are available in my current execution context?”
I have thought about this often as one of the biggest challenges in working with Ruby projects “at scale”, and for my particular areas of interest, this is more of a factor weighing against Ruby than the oft-discussed topic of performance.
To add to my initial comment, I’ve seen posts from other companies that describe how they’ve managed to use engines to encapsulate and enforce interfaces between parts of their application. Flexport and Root Insurance come to mind.
Hi! This is a great topic that I should have included in the blog post. Rails Engines is definitely one of the mechanisms you can use to modularize a Rails application. As @swifthand mentioned below, Packwerk is a tool to enforce boundaries. While Rails Engines comes with other functionalities, it can only be used to establish boundaries. The modularity isn’t enforced because constants will still be globally accessible.
You can try using both Packwerk and Rails Engine in a Rails app though. What do you think?
I also highly recommend checking out the Rails engine section in my colleague’s blog post - https://engineering.shopify.com/blogs/engineering/shopify-monolith