The Multi-App Workspace: Sharing UI Without Sharing Domains In short: A monorepo makes every import possible, including the ones that ruin you. Tag by domain and by layer, declare what may depend on what, and let the linter enforce it — otherwise the farmer portal ends up on the dispatch team's release schedule. We have presentation models, adapters, and facades. All of that discipline collapses if the file system lets a developer in the farmer portal auto-import a dispatch service. The spaghetti monorepo In a poorly structured workspace, everything is reachable. A developer on the farmer portal needs to work out a tanker's usable volume, searches the workspace, and finds in the dispatch domain. Their editor offers it. They take it. Nothing failed. The build passed, the tests passed, the feature shipped. What happened is that the farmer portal is now coupled to the dispatch team's release schedule. Six months later, dispatch changes the calculation to account for tanker pressure — correct for their regulatory requirement, wrong for farmer payouts. The farmer portal starts paying suppliers slightly wrong amounts, and nobody connects it to a dispatch PR. That is domain bleeding, and it is invisible until it costs money. Structure by scope and type Every project gets two tags: which business domain owns it, and where it sits in the layering. Where the pieces from this chapter live: Piece Library Why --- --- --- , Presentational, zero business context A dispatch business type adapter Knows both sides — so it belongs to the consumer Feature state HTTP The adapter placement is the interesting one. It is the only code that legitimately knows both a DTO and a presentation model, which is precisely why it belongs to the feature — the one place where coupling to both is the job. The rules Three of those lines do the heavy lifting: → only , . The shared library cannot import a DTO. Lesson one's bug is a lint failure. → only , . The farmer portal cannot import from dispatch. → only . Shared code cannot reach into a domain, which stops slowly acquiring dependencies on everything. When two domains genuinely need the same thing The linter says no. That is the point — but it is not the end of the conversation, and if you treat it as one, people will find ways around it. Both apps genuinely need to format litres. Three legitimate resolutions: 1. Promote it to shared. If it is business-agnostic — number formatting, date helpers, a type — move it to and tag it . This is right for . 2. Duplicate it. If the two versions will diverge, let them. Dispatch's volume calculation accounting for tanker pressure and the farmer portal's payout volume are not the same rule — they only look alike today. Two functions that can evolve independently are cheaper than one that both teams have to agree on. This is Chapter 2's Rule of Three arguing for restraint. 3. Go through the backend. If the farmer portal needs dispatch data , it calls a dispatch API. It does not import dispatch code. A shared frontend import couples release schedules; an HTTP call does not. The failure mode to avoid is a fourth option nobody proposes out loud: adding to the farmer portal's allowed tags to unblock a deadline. That is not a workaround, it is deleting the rule. Catching cycles Boundary tags miss one thing: two libraries inside the same tag importing each other. Add a graph check: Cycles inside a domain are the ones that make a build slow and a refactor impossible, and they never trip a tag rule. What this buys at release time With clean boundaries, the dependency graph tells you what a change affects — which is what makes Chapter 8's CI affordable: A change to cannot affect the dispatch dashboard, and the tooling knows it because the boundary makes it true. Without the boundaries, everything is potentially affected, so everything runs, and the pipeline takes forty minutes. "But I thought the whole point of a monorepo was easy sharing" This is the genuine appeal of a monorepo and it is not wrong — one checkout, one version of the tooling, atomic cross-project changes, no publishing dance to test a library change. Those are real and they are why we are here. The confusion is between sharing being possible and sharing being unrestricted . A monorepo removes the mechanical barriers to sharing: no npm publish, no version negotiation, no waiting for a release. That is the benefit, and boundary rules do not take it away — promoting to is still a five-minute change with no publishing step. What boundary rules remove is accidental sharing. The import that happened because an editor offered it, not because anyone decided two domains should be coupled. The distinction is who made the decision: Deliberate sharing Accidental sharing --- --- --- How it happens Someone moves code to Autocomplete offered a path Reviewed Yes — a PR that moves a file No — one line in an import block Is it business-agnostic? Checked Nobody looked Who owns it afterwards The shared library Nobody…