In September, 2020, our staff at Shopify launched a Ruby gem named Packwerk, a instrument to implement boundaries and modularize Rails functions. Since its launch, Packwerk has taken on a lifetime of its personal, inspiring weblog posts, convention talks, and even a complete gem ecosystem. Its reputation is a sign that Packwerk clearly stuffed a void within the Rails neighborhood.
Packwerk is a static evaluation instrument, just like instruments like Rubocop and Sorbet. Utilized to a codebase, it analyzes fixed references to assist decouple code and arrange it into well-defined packages.
However Packwerk is greater than only a instrument. Through the years, Packwerk’s strategy to modularity has come to embody distinct and generally conflicting views on code group and its evolution. Packwerk’s suggestions can change all the trajectory of a codebase to a level that distinguishes it from different instruments of its type.
This retrospective is our effort, because the staff that developed Packwerk at Shopify, to shine a light-weight on our learnings working with the instrument, considerations about its use, and hopes for its future.
Origins of Packwerk
Packwerk as a Dependency Administration Software
“I do know who you might be and due to that I do know what you do.” This information is a dependency that raises the price of change.
– Sandi Metz, Sensible Object-Oriented Design in Ruby
Sandi Metz’ quote above captures the spirit from which Packwerk was born. The premise is straightforward. To make use of Packwerk, you will need to first do two issues:
- Outline a set of packages, captured in (probably nested) file directories.
- Outline a non-circular set of dependency relationships between these packages.
With this completed, you possibly can then run Packwerk’s command-line instrument, which is able to let you know the place constants from one bundle reference constants from one other bundle in ways in which violate your said dependency graph. Violations may be quickly “allowed” through todo recordsdata (
package_todo.yml); this makes it attainable to “declare chapter” in a codebase by producing a todo file for current violations and utilizing Packwerk to forestall new ones from creeping in.
The pursuit of a well-defined dependency graph on this manner ought to, in concept, make software code extra modular and fewer coupled. If a bit of an software must be moved, it may be completed extra simply if its dependencies are explicitly outlined. Conversely, round dependencies tangle up code and make it extra obscure and refactor.
Packwerk as a Privateness Enforcer
Within the metaphor of carrots and sticks, privateness is sugar. It’s straightforward to grasp and has broad enchantment, however it might not really be good for you.
– Philip Müller, unique writer of Packwerk (hyperlink)
Packwerk acquired a completely completely different utilization in its early phases, within the type of “privateness checks” which may very well be enabled on the identical set of packages above to statically declare public APIs. Constants that have been positioned in a separate public listing have been handled as “public” and may very well be referenced from some other bundle. Different constants have been thought-about “personal” and references to them from different packages have been handled as violations, no matter dependency relationships.
As expressed within the quote above by Philip Müller, privateness checks have been by no means supposed to be the primary characteristic of Packwerk, however it’s straightforward to see their enchantment. Dependencies in massive sprawling codebases may be troublesome to accurately outline, and even more durable to resolve. Declaring a relentless public or personal, in distinction, is straightforward, and intently resembles Ruby’s personal idea of personal and public strategies.
Sadly, whereas straightforward to make use of, Packwerk’s privateness checks launched a number of issues. A few of these have been issues of implementation: the checks required a separate
app/public listing for code that was meant to be public API. This broke Rails conventions on file structure by introducing a folder beneath
app that denoted privateness degree as a substitute of structure ideas. Confusion round the place recordsdata ought to go resulted in new subdirectories being created for controllers and jobs, duplicating those who already existed beneath
app. As public API, these subdirectories ought to have been documented and nicely thought-out, however Packwerk didn’t encourage this degree of element. Thus, we ended up with infinite poorly-documented public code that was by no means meant to be public within the first place.
But there was a deeper drawback, specifically that privateness checks had remodeled Packwerk into one thing it was by no means supposed to be: an API design instrument. Packwerk was getting used to make sure that packages communicated through blessed entrypoints, whereas its unique goal was to outline and implement a dependency graph. Bundle A utilizing bundle B’s code (even its public API) isn’t acceptable if bundle A doesn’t rely upon B, but we discovered builders have been specializing in the design of their APIs over the dependencies of their code. This was drawing consideration away from the issue the instrument had been created to unravel.
Given these points, privateness checks have been faraway from Packwerk with the launch of model 3.0.
Weaknesses and Blind Spots of Packwerk
We now have discovered that the largest points with Packwerk are associated to what the instrument does not do for you: what it can not see, what it can not know, and what it doesn’t let you know.
Utilizing Packwerk begins with declaring your packages: what code goes the place, and the way every set of code is dependent upon the remainder. The selection of packages and their relationships may be fiendishly troublesome to get proper, significantly in a big codebase the place traditionally all the pieces has been world. When you can change your bundle definitions later, any such adjustments include a possible value when it comes to the effort and time spent isolating code that now finally ends up again collectively. Packwerk gives no steering right here, and is pleased with any alternative you make. It would generate for you a set of todo recordsdata that get you to your said objective. Whether or not this work will really get you to a greater place, nevertheless, is one other query fully.
Pushing the accountability of drawing the dependency graph for an software onto the developer can usually result in incorrect assumptions on how code is coupled. That is significantly true should you solely work with one part of a bigger codebase, or don’t have an excellent grasp on dependency administration and code structure.
We now have discovered that builders are inclined to group code into packages based mostly strongly on semantic clues that in lots of circumstances have little relation to how the code really runs. We now have a mannequin in our monolith, for instance, that holds “store billing settings”, together with whether or not a store is fraudulent. This mannequin was positioned in a “billing” bundle by advantage of its identify, however this was the flawed place for it: detecting fraudulent outlets is crucial to dealing with any store request, not simply these associated to billing particulars. Our answer was to disregard the semantics of its identify and transfer it to the bottom of our dependency graph, making it accessible to any controller.
This type of choice is difficult as a result of it goes in opposition to our instinct, as people, to abide by the naming of issues. Packwerk operates fully on the premise of the high-level view of the codebase we offer it, which is commonly strongly influenced by this instinct; if the graph of dependencies it sees is misaligned with actuality, then the trouble builders exert resolving dependencies could deliver little to no profit. Certainly, such efforts could even make the code worse by introducing indirection, rendering it extra difficult and more durable to grasp.
Even assuming a well-drawn dependency graph, the issue arises of how to resolve violations. Packwerk doesn’t present suggestions on how to do that; it solely sees fixed references and the way they relate to the set of packages you’ve got offered. This makes it troublesome to know should you’re doing the precise factor or not when approaching fixes for dependency violations.
There are additional blind spots that may make these issues worse. Like different static evaluation instruments, Packwerk is unable to deduce constants generated dynamically at runtime. Nonetheless, Packwerk has a much more limiting hole in its image of software constants due to its dependence on Zeitwerk autoload directories. Constants loaded utilizing mechanisms like
ActiveSupport::Autoload are untracked and invisible to the instrument. Because of this, a bundle that’s well-defined in keeping with Packwerk (has no violations left to resolve) may very well crash with identify errors when its code is executed.
Additional to Packwerk not seeing the total image, should you’re utilizing full Rails engines as packages like us, it doesn’t assist with sorting via routes, fixtures, initializers, or something exterior of your app listing. Something that isn’t referenceable with constants turns into implicit dependencies that Packwerk can’t see. This usually causes extra issues which are solely seen at runtime.
A Bundle with Zero Violations
The blind spots talked about above grow to be the obvious once you really try and run packaged code in isolation. Working in “isolation” right here means loading a bundle along with its dependencies and nothing else. In concept, a bundle that has no violations, whose dependencies themselves additionally haven’t any violations, ought to be usable with out some other code loaded. That is the purpose of a dependency graph, in spite of everything.
Lately, we determined to place Packwerk to the check and truly create such a bundle. To maintain issues easy, we selected for this check the one a part of our monolith that ought to, by definition, haven’t any dependencies. This “junk drawer” of code utilities,
named “Platform”, holds the low-level glue code that different packages use. Platform’s place on the base of the monolith’s dependency graph made it an apparent alternative for our first isolation effort.
Platform, nevertheless, was not even remotely remoted after we began. Having a clear slate was vital, so fairly than start with Platform itself, we as a substitute carved out a brand new bundle beneath it that may solely include its most important components. Into this bundle, which we named “Platform Necessities”, we moved base lessons like
ApplicationRecord, together with the infrastructure code that different components of the monolith relied on to do just about something. Platform Necessities can be to our monolith what Lively Assist is to Rails.
The train to isolate this bundle was an eye-opener for us. We achieved our objective of an remoted base bundle with zero violations and nil dependencies. The method was not straightforward, nevertheless, and we have been compelled to make many tradeoffs. We relied closely on inversion of management, for instance, to extract bundle references out of base layer code. These adjustments launched indirection that, whereas resolving the violations, usually made code more durable to grasp.
We have been greeted on the zero violation objective line with a stunning discovery: a bug in Packwerk. Packwerk was not cleansing up stale bundle todos when all violations have been resolved. The truth that this bug, which we patched, had been nearly unnoticed till then indicated that we have been possible the primary Packwerk person to fully work via a complete bundle todo file, years after its preliminary launch. This confirmed our suspicion that the speed at which Packwerk was figuring out issues to its customers vastly outpaced their capability to really repair them (or curiosity in doing so).
Having resolved all Packwerk violations for our base bundle, we then tried to really run it by booting the monolith with solely its code loaded. Unsurprisingly, given the problems talked about within the final part, this didn’t work. Certainly, we had but extra violations to resolve in locations we had by no means thought-about: initializers and surroundings recordsdata, for instance. As talked about earlier, we additionally needed to deal with code that was loaded with out Zeitwerk, which Packwerk didn’t observe. We fastened these points by shifting initializers and different software setup into engines of the appliance, in order that they weren’t loaded after we booted the bottom layer by itself.
With boot working, we went a step additional and created a CI step to run assessments for the bundle’s code in isolation. This surfaced but extra points that neither Packwerk’s static evaluation nor boot had encountered. With assessments lastly passing, we reached an inexpensive confidence degree that Platform Necessities was genuinely decoupled from the remainder of the appliance.
Even for this comparatively easy case of a bundle with no dependencies, our effort to achieve full isolation had taken many months of laborious work. On the one hand, this was far multiple would possibly count on for a single bundle, hinting on the daunting scale of dependency points left to deal with in our monolith. The truth that a lot work remained to be completed even after resolving dependency violations was a sign of Packwerk’s limitations and the extra tooling wanted to fill gaps in its protection.
In reality, although, the train was probably not about Packwerk. It was about isolation, and whether or not such a factor was even attainable in a codebase of this measurement, constructed on assumptions of worldwide entry to all the pieces. And on this query, the train had been a powerful success. We did one thing that had by no means been completed earlier than in a timespan that had a concrete completion date. We applied checks in CI to make sure our progress would by no means be reversed. We had made actual, tangible progress, and Packwerk, given the precise context, had performed a key position in making that progress a actuality.
Area versus Operate in Packages
Shopify organizes its monolith into code models referred to as “parts”. Parts have been created a few years in the past by sorting 1000’s of recordsdata into a pair dozen buckets, every representing its personal area of commerce. The monolith’s codebase was thus divided into directories with names like “Supply”, “On-line Retailer”, “Merchandising” and “Checkouts”. With such a big change, this was an effective way on the time to partition work for groups, restrict new part creation, and produce order to a codebase with thousands and thousands of traces of code.
Nonetheless, we rapidly found that domains and the boundaries between them don’t replicate the way in which Shopify’s code really capabilities in follow. This was instantly apparent when working Packwerk on the codebase, which generated monstrously massive todo recordsdata for each part. With each new characteristic added, these todo recordsdata grew bigger. Builders may resolve a few of these violations, however usually the fixes felt unnatural and overly difficult, like they have been going in opposition to the grain of what the code was really making an attempt to do.
There was an vital exception, nevertheless. The monolith’s Platform part, described earlier, was from the beginning a purely system-level concern. Together with a pair others prefer it, this part by no means match into the mould of a “commerce area”. This made it an oddball in a domain-centric view of the world. Once we shifted our focus to really working code, versus merely sorting it, the purely practical nature of this part abruptly grew to become very helpful, nevertheless. In contrast to each different part, Platform’s place within the dependency graph was apparent: it should sit on the base of all the pieces, and it will need to have zero dependencies.
The concentrate on working code has instigated a rethink of how we arrange our monolith. We’re confronted with a dichotomy: some parts are domains, whereas others are designed across the practical position they play within the software. A checkout move is a perform outlined because the code required for a buyer to provoke a checkout and pay for his or her order. Our “checkouts” part, nevertheless, comprises quite a few considerations unrelated to this move, equivalent to controllers and backend code for retailers to switch their checkout settings. This code is a part of the checkout area, however not part of checkout move performance.
Truly working packages in isolation requires them to be outlined strictly on a practical foundation, however most of our parts are outlined round domains. Lately, our answer to this has been to make use of parts as top-level organizational instruments for grouping a number of packages, fairly than a singular code unit. This manner, groups can nonetheless personal domains, whereas particular person packages act because the actually modular code models. It is a compromise that accommodates each the human want for comprehensible psychological fashions and the runtime want for well-defined models of a dependency graph.
Packwerk is a Sharp Knife
When making an attempt to modularize a big legacy codebase, it’s straightforward to get carried away with concepts of how code ought to behave. Packwerk lends itself to this tendency by permitting you, the developer, to outline your required finish state, and have the instrument lead you to that objective. You resolve the set of packages, and you resolve the dependency graph that hyperlinks them collectively. Simply work down the todo file, and you’ll attain the code group you need.
The issue with this view is that it’s laborious to know if it’ll result in concrete outcomes. Code exerts a robust drive within the path of perform. It’s a lot more durable to bend this habits to suit your psychological fashions than it’s to bend your psychological fashions to suit what a codebase really does.
We discovered this lesson the laborious manner. We began with a utopian imaginative and prescient for our monolith, with modular code models representing domains of commerce and cleanly-defined dependencies relating them to one another. We constructed a instrument to chart a course to our objective and utilized it to our codebase. The work to be completed was clear, and the trail ahead appeared apparent.
Then we really sat right down to do the work, and issues started to look rather a lot much less rosy. With hard-fought features and messy tradeoffs, we made it via the todos for a single bundle, solely to seek out that we have been possible the primary to achieve the end line. Our achievement turned out to be bittersweet, since our code was nonetheless damaged and unusable in isolation. The utopia we had imagined merely didn’t exist, and the instrument we thought would get us there was main us astray.
What turned this example round for us was the conclusion that working code, greater than any metric, will all the time be one of the best indicator of actual progress. Packwerk has its place, nevertheless it is only one instrument of many to measure facets of code high quality. We achieved a small however vital victory by being extremely pragmatic and broadening our understanding to leverage an strategy we hadn’t initially thought-about.
Like many different instruments within the Rails ecosystem, Packwerk is a pointy knife, and it have to be wielded with care. Be intentional about how you utilize it, and the way you repair the violations it raises. All the time ask your self if the violation is an error on the developer degree, or on the dependency graph degree. Whether it is on the graph degree, contemplate adjusting your bundle structure to higher match the dependencies of your code.
At Shopify, we regularly stress check our assumptions and revisit the choices we made prior to now. We now have mentioned eradicating Packwerk from our monolith, given the prices it incurs and the weaknesses and blind spots described earlier. For us, the technical debt launched from privateness checking remains to be a great distance from being paid off. Packwerk has nevertheless offered worth in holding the road in opposition to new dependencies on the base layer of our software. Nonetheless imperfect, its checklist of violations to resolve is an efficient method to divvy up work towards a well-defined isolation objective.
Our learnings utilizing Packwerk have knowledgeable a bigger technique for modularizing massive Rails functions, one that’s strongly oriented towards working code and executable outcomes fairly than philosophical beliefs. Whereas now not as central because it as soon as was, Packwerk nonetheless performs a job at Shopify, and can possible proceed to take action over time to return.