Shopify chose to keep its large Rails codebase as one deployable but enforce boundaries inside it, and built a tool called Packwerk to check them. What does that checker catch, why does a rule on paper not suffice, and when would copying the approach be a mistake?
Show the full answer Hide the answer
The situation
Shopify's engineering blog describes a codebase that began as a monolith with no boundaries between its parts. Ruby and Rails give little help here: any code can call any other code, so, in the blog's account, large Rails applications drift toward tight coupling where a change in one area breaks unrelated code. Shopify chose to evolve toward a modular monolith, which means one codebase and one deployable but with defined components, rather than splitting it into many separately deployed services.
What the tool checks
Packwerk reports two kinds of violation, as the Shopify post describes them. A dependency violation is when a package uses a constant from another package that it has not declared as a dependency. A privacy violation is when outside code uses a package's private constants. Run in the build, both fail the check.
This is a fitness function: an automated test of an architectural property rather than of behaviour. Boundaries in a wiki page are broken within weeks, because the shortest path to a feature is a direct call across the line and nobody reviewing a 40-file change spots one extra import. A failing build catches it on the pull request.
Why it fit
Shopify had a large codebase and many teams, and the cost of a network boundary (latency, deployment, distributed failure) was not what it was trying to buy. It wanted ownership and change isolation. A check in the build gives that at the cost of a tool and some friction.
What it cost
- Retrofit pain: a codebase with years of cross-calls starts with a long list of violations. The standard technique is to record the current ones as a baseline, fail the build only on new ones, and shrink the list over time.
- Friction: developers meet a failing check for a legitimate shortcut and need a fast route to propose a boundary change.
- A boundary the tool cannot see: shared database tables can still couple packages, which the post itself notes as a remaining gap with shared models.
Common weak answers
- "Boundaries are a documentation job." Documented boundaries erode; only a failing check holds them.
Where copying it would be a mistake
A 10-person team on a small codebase does not need a package checker; a folder convention and code review hold the line. And a boundary check does not give independent deployment or independent scaling: if one component needs 50x the CPU of the rest, one deployable still scales as one.
What a strong answer adds
Mention the ratchet: the allowed-violation count may only fall, and a rising count blocks the merge.