Skip to content
C.W.K.
Stream
Lesson 03 of 04 · published

Separate Apps, One Plumbing

~11 min · architecture, monorepo, boundaries, decision

Level 0Loose Parts
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

The Two Bad Answers

With the inventory on the table and the cross-app bug fresh, there are two answers that present themselves immediately, and both are worse than they look.

Merge the apps. One codebase, one deploy, no duplication by construction. It solves the stated problem completely and creates a bigger one: three domains that genuinely disagree now have to share a data model, a navigation structure, and a release cadence. A travel app organizes everything around a journey with a start and an end. A journal deliberately has no container at all — its whole identity is an open timeline. A health log refuses terminal states because a reading is never a verdict. These are not skinnable variants of one program. Merging them would trade a duplication problem for a modeling problem, and modeling problems do not have a script that fixes them.

Do nothing and be disciplined. Agree that the shared files must be kept in sync by hand, and rely on everyone remembering. This is the option that was in effect on the day of the measurement, and the measurement is what it produced.

The Ruling

The call, made the same day the inventory was taken: the apps stay separate, because their domains are genuinely different. The shared plumbing gets exactly one owner — a repository whose entire job is to hold the code that is identical across siblings by design, and to deliver it.

Notice how narrow that is. It is not "share what happens to be similar". Similar-looking code in two apps is often two correct answers to two different questions, and forcing them together produces a parameter-riddled function that serves neither. The scope is code whose correct shape does not depend on what the app is about — which is a much smaller set, and a much more defensible one.

Extract along the seam that already exists. The four triplicated layers were not chosen by an architect looking for reuse opportunities. They were chosen by the copies themselves: those were the files that had been copied verbatim, because they were the files that did not need editing to work in a new app. The duplication inventory is not just evidence that a problem exists — it is a map of where the natural boundary is.

The Mirror Rule, Stated at Birth

A repository that holds shared code exerts a constant pull. Every time two apps need something similar, the cheapest move is to put it in the shared place, and each individual instance of that is defensible. The end state, reached one defensible step at a time, is a framework: something every app must be built around, that knows what apps are about, and that nobody can change without changing all of them.

So the rule was written in the same breath as the repository's purpose, pointing the other way: domain code never migrates into the kit. Plumbing in, domain never out of its app. The sibling architecture documents already forbade the apps from copying each other's domain spines — one of them says outright that the journey/day axis must not be copied over unexamined — and a shared repository is exactly the backdoor that would let that happen without anyone noticing they were doing it.

Code

What the ruling looks like as a layer diagram·text
         travel app          journal app         health app
      ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
      │  journeys    │    │ open         │    │ readings     │  <- DOMAIN
      │  lanes       │    │ timeline     │    │ modules      │     never
      │  plan vs     │    │ journals     │    │ observe,     │     shared
      │  actual      │    │ (no          │    │ never        │
      │              │    │  container)  │    │ diagnose     │
      ├──────────────┤    ├──────────────┤    ├──────────────┤
      │  shim        │    │  shim        │    │  shim        │  <- APP-OWNED
      │  (binds kit  │    │              │    │              │     ~10 lines
      │   to app     │    │              │    │              │     each
      │   constants) │    │              │    │              │
      └───────┬──────┘    └───────┬──────┘    └───────┬──────┘
              │                   │                   │
              └───────────────────┼───────────────────┘
                                  │
                      ┌───────────▼───────────┐
                      │   THE KIT (one owner) │
                      │  assistant client     │
                      │  publish client       │
                      │  encryption at rest   │
                      │  id minting           │
                      │  settings store       │
                      │  outbox, clipboard    │  <- PLUMBING
                      │  shared UI atoms      │     identical by design
                      └───────────────────────┘

  The arrows only point UP. A kit module that needed to know which
  app it was running in would be an arrow pointing down, and that is
  the shape the mirror rule exists to refuse.

External links

Exercise

Take the inventory you produced in lesson 1 and sort every entry into exactly two piles: identical-by-design (the correct implementation does not depend on what the app is about) and merely-similar (it does). Then write one sentence for each merely-similar entry explaining what product decision is embedded in it. That sentence is the reason it must not be shared.
Hint
If you cannot find the embedded product decision, the entry probably belongs in the first pile after all. And if a single entry contains both — a generic mechanism wrapped around an app-specific policy — that is the signal to split the file before deciding where it lives.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.