advanced 3 min answer

Shopify's engineering blog introduced Packwerk as a static-analysis gem that enforces package boundaries inside its Rails monolith. Its README states that Packwerk only cares about static constant references and that method calls and objects passed around the application are completely ignored. What does that design buy a platform team that wants to run the check as a blocking gate, and where would copying it be a mistake?

shopifypackwerkguardrailsstatic analysismodular monolith
Show the full answer Hide the answer

The situation they were in

A monolith that many teams edit has no enforced internal structure. Ruby offers almost no boundary enforcement of its own, so a package boundary lives in a wiki page and in the memory of whoever wrote it. The boundary is not weak because people disagree with it; it is weak because nothing counts the crossings.

What the design actually buys

Restricting the checker to statically resolvable constant references is what makes the check runnable on every pull request. A pass that only resolves constants reads the source without executing it, so it finishes in seconds on a few hundred thousand lines and needs no test environment, no database and no network. A checker that had to observe real call graphs would need a running system and a representative workload, which turns a 10-second pull-request check into a nightly job that nobody blocks on.

So the trade is explicit and in the platform's favour: a narrower question, answered fast enough to be asked at authoring time, beats a complete question answered after merge.

What it cost them

The ignored half is invisible. Dynamic dispatch, send, a constant built from a string, a class reopened somewhere else: every one of those crosses the boundary with a green check beside it. The failure mode is not an alarm but a belief - the team reads a passing build as "the boundary holds" when the correct reading is "the boundary holds for references the parser can see".

Second cost: a boundary enforced only at authoring time says nothing about what is already running. Code merged before the rule existed keeps working, which is intentional, and means the check measures the flow of new violations rather than the stock of old ones.

Where copying it would be a mistake

Match the enforcement mechanism to the cost of a false negative. A missed import that couples two domains is annoying and fixable next sprint, so a static check is the right strength. A missed access to another tenant's data is not recoverable, and no static checker should be the only thing standing in its way: that boundary needs a mechanism the code cannot route around, such as a separate database credential per package or a per-tenant connection scoped by the request context.

The decision rule: make a check blocking when violations are cheap to fix at authoring time and a false negative is survivable; where a false negative is not survivable, the control belongs at runtime and the static check is only an early warning. Teams that invert this get a blocking gate on something harmless and an advisory warning on the thing that would end the company.

Common weak answers

  • "Add dynamic analysis so the check is complete." It is no longer the same control. It runs after the fact, it needs production-shaped traffic to see rare paths, and its findings arrive in a report rather than in a diff - which is a different and weaker intervention, however complete.
  • "Nothing that incomplete should block a build." Incompleteness is not the test. A compiler's type check is incomplete and still worth blocking on. The test is whether the violations it does catch are worth catching at that moment.
  • "Write the boundary in the architecture decision record and review it." That is the state Packwerk exists to leave, and it degrades at the first forty-file pull request.