Best Practices for Modularization: When to Use Engines and Gems in Addition to Packwerk?

<@U78EF4XFY8W> I’m actually about to kickoff a greenfield project and am convinced I will need some “Big Rails” support to prevent the Big Ball of Mud the company current uses.

Message excluded from import.

I created <#C04094L8UQ5|greenfield-development>, let’s chat more there about this broad category of problems!

@palkan

I would say that choosing gems/engines vs packwerk-only is like choosing between strong and eventual consistency.

I like the terms, but think that one of the solutions (packages) are strictly more powerful in that they can do both (strong and eventual): With the addition of package protections you can choose to make a new package that is fully protected, doesn’t allow for violations, and is such “strong” in your terms. Note that we may need to add a couple more protections to fully achieve this vision

that is fully protected
In Ruby, static analysis cannot guarantee full protection. Of course, you can say “no” to metaprogramming of any kind to avoid possible breaches; but that would leave with a less powerful version of Ruby. Similarly, adding types could help with protection, but would affect the way we write programs.

For me, the major drawback of the current Big Rails approach is that along with the great features (being much easier and more efficient for gradual adoption in the existing codebases) it brings the necessity to re-invent solutions to many (isolation/protection) problems which could be naturally solved by using Ruby gems—a mechanism that always was and always be here. And gemming (theoretically) evolves with the language, while tools must be supported by other developers.

For example, Shopify has been using their own components implementation, but eventually decided to go with engines (now sure what is the state of their deconstruction in 2022). Will Big Rails repeat the fate of Shopify components or not? I’m not sure.

Engines are far from perfect and require some rituals to work smoothly. In 2020, I hoped that the Shopify move would be the first step towards new engines, towards modularization built into Rails. That didn’t happened yet.

Message excluded from import.

Not sure I got the question

Message excluded from import.

I would ask the opposite: which parts of a package should be considered public? There are different approaches, from considering everything public (so, no protection) to exposing only a specific public abstraction layer, SDK (in this case, the package could be seen as a remote service with an RPC-like communication API; you don’t even allow models to be used outside of it, and use some mapping instead).

If the primary goal of modularizatation is to scale teamwork (so, different teams work on different components independently), then you should aim to keep your public part as small as possible. In this case, we try to avoid adding changes (especially breaking) to public APIs, but can do whatever we want with the internals.

Message excluded from import.

Message excluded from import.

@palkan and Scott — thanks for all the comments!

In Ruby, static analysis cannot guarantee full protection
True — I think @stephan meant something like — we can use package protections to not permit any boundary exceptions to occur of the kind that packwerk can identify (versus permitting a TODO list). Keep in mind that gems and engines also don’t guarantee full protection. Two examples: (1) I reference a constant in my gem that doesn’t exist in the gem, only in the client, and I don’t have proper test coverage. I’ve violated a boundary without protection. (2) I use a method that comes from activesupport without including activesupport in my gemspec, and my code is not fully tested. What we’ve found is the static analysis approach gets us the majority of the way there with a fraction of the cost (I can’t stress how much cheaper it is to maintain — let’s zoom about this if you’d like more info). If you’ve resolved everything that packwerk has identified, you can always apply other tools on top of that.

which could be naturally solved by using Ruby gems

At least with gems so far — it can’t! Gems require all-or-nothing modularization approaches. A gem can only depend on other things which are fully gemified. Likewise, code can only be made a gem if it all of its dependencies are fully gem-ified. Note that Shopify created packwerk and found engines were not suitable for their problems. If someday gems/gemspecs adopt the functionality we want from packages (gradual modularization primarily), and we are able to move to gems, that would be a huge win in my book!

Yes — at Gusto we’ve said that everything is private unless explicitly private. This is a huge shift from what’s default in Ruby — that everything is public unless made private.

Message excluded from import.