@stephanhagemann.com Can you talk a little more about managing the "hidden" coupling of two packs using the same ActiveRecord model?Your post assumes they don't (it seems), but this sounds like a common occurrence in discussions of Packwerk that I've... source
My book Gradual Modularization for Ruby and Rails is about using Packwerk for, well, gradual modularization. While I wrapped up the manuscript about six months ago, the bulk of the innovation over the things I thought about in Component-based Rails Applications happened in 2021 and 2022. It has been a while since I took a long look at the merits of the underlying tool. This year, I’ve realized there are two very different ways to use Packwerk.
My book Gradual Modularization for Ruby and Rails is about using Packwerk for, well, gradual modularization. While I wrapped up the manuscript about six months ago, the bulk of the innovation over the things I thought about in Component-based Rails Applications happened in 2021 and 2022. It has been a while since I took a long look at the merits of the underlying tool. This year, I’ve realized there are two very different ways to use Packwerk.
Mode #1: Creating Gradual Modularization
This is the “classic” way of using Packwerk, the one I wrote my book about. It goes something like this:
- Identify a part of the application (a package) that makes sense to isolate.
- Apply Packwerk’s tools: dependency checks, privacy checks—in fact, use all the enforcements that seem to help
- Accept sensible dependencies via the
package.yml
. - Expose a carefully designed public APIs via the
public
folder or file annontations. - Work to remove all violations.
It’s about shaping the monolith into better-structured made of smaller parts with more or less defined boundaries.
This is Packwerk for gradual modularization.
Mode #2: Preparing Service Extraction
Then at work, Gusto, we shifted focus: From modularizing the monolith to extracting actual services. That’s when I realized there’s another mode of using Packwerk.
Here’s how it looks:
- Identify a part of the application that you’re convinced should be its own service.
- Apply dependency and privacy checks. Use all the enforcements.
- Accept no dependencies (leave the
dependencies
key inpackage.yml
empty). - Expose no public APIs (leave the
public
folder empty). - And again: remove all violations!
Wait—how is that supposed to work?
Instead of creating a Ruby API with privacy, you flip the logic:
- Use privacy to enforce that the package has no incoming Ruby dependencies. (An extracted service cannot be invoked as Ruby code, after all.) The public API is intentionally empty.
- Use dependency checks to enforce that the package has no outgoing Ruby dependencies. Again, extracted services don’t directly call other parts of the monolith.
Once that’s enforced, you start replacing the existing Ruby calls with, whatever it is that you have decided your services are going to use to communicate. This could be gRPC, GraphQL, REST—whatever protocol you’re adopting. You could switch from synchronous interactions to asynchronous ones and use RabbitMQ or Kafka. Do this for the to-be-extracted service and all* the code that calls it or gets called from it (*see postsript below)
This is Packwerk for service extraction.
Postscript
Gem dependencies
One corollary (and this ties back to this earlier post): if you depend on code that you’re convinced should be a gem, don’t wrap it in a gRPC or REST API. Instead, extract it into a gem, have the app depend on that gem, and since Packwerk doesn’t look inside gems, you’ve effectively removed the violations.
The path through inter-service API generation
I believe there’s a potential (maybe theoretical) path from Mode 1 to Mode 2, though I haven’t seen it in practice:
Imagine a modular monolith where packages have fully developed public APIs, free of violations. Now imagine further that all of those APIs are statically typed. In such a system, you could auto-generate service boundaries: REST or gRPC APIs derived directly from the Ruby package APIs.
The tricky part would not be generating the APIs—it would be refactoring the clients. Error handling, transaction boundaries, and other runtime concerns would all have to be carefully rethought.
Would that be overall helpful? I don't know. Have you seen this in practice?
#packwerk #gradual modularization #monolith #architectureSocial
1 Reply
These are webmentions via the IndieWeb and webmention.io. Mention this post from your site: