Monorepos: shared code, explicit boundaries
On this page
My earlier explanation of monorepos used a forest as a metaphor for projects sharing resources. That is a useful starting point, but it leaves out the decision that matters: which parts of the code should change together, and which parts need to remain independent?
A monorepo holds multiple projects in one repository. It can make a shared change easier to inspect and coordinate. It does not, by itself, make the projects modular, align their dependency versions, or make every deployment safe.
What is actually shared?
A repository can contain applications alongside the libraries they use. The original directory structure makes that relationship easier to see:
root/
├── packages/
│ ├── ui-components/
│ ├── shared-utils/
│ └── core-services/
├── apps/
│ ├── web/
│ ├── mobile/
│ └── admin-dashboard/
└── package.jsonThe applications can depend on shared packages without reaching into one another’s implementation. A shared UI package is useful when its consumers need the same behavior. Putting unrelated code into shared-utils simply because it fits there creates a dependency without a clear purpose.
Turborepo’s package model distinguishes deployable applications from the libraries that support them. That distinction is more useful than a folder convention alone: it helps describe what can depend on what.
One commit is not one deployment
An atomic source change can update a library and its consumers in the same commit. A reviewer can inspect the interface change, callers, and tests together rather than coordinating several repository changes.
That is a source-control boundary, not a distributed transaction. Applications may still deploy at different times. If a browser client, service, or database schema changes, a mixed-version period may still exist. Design compatibility and rollout around the deployed system, not the fact that its source shares a repository.
Shared dependencies still need a policy
Enforce the boundaries you draw
A package should declare what it depends on and expose the interface its consumers are meant to use. Cross-package imports that bypass that interface make refactoring harder, even though all the files are easy to reach.
Automated boundary checks can detect undeclared dependencies and imports outside a package. Turborepo’s boundaries command is currently experimental. Whatever tool a team chooses, ownership and review rules still need to be clear.
Security boundaries need separate treatment. A server-only identity helper, for example, should not become a browser dependency because it lives in a convenient shared package. The authentication synchronization example depends on enforcing identity and permissions where the operation runs.
Account for the cost of shared tooling
Choose around the work, not the label
A monorepo is worth considering when related projects frequently change together and teams benefit from a shared view of their interfaces. Separate repositories can be a good choice when access requirements, ownership, or release coordination make that separation useful. Neither layout guarantees independence or collaboration.
Before migrating, look at a few real changes. How many projects did each touch? Where did coordination stall? Which tests would establish that the shared change was safe? Those answers are a better basis for the decision than the repository style of a much larger company.
The same questions matter in AI-assisted engineering: a clear module interface and useful feedback let implementation move without asking a reviewer to reconstruct the entire system. More shared code should not mean more hidden context.