Shared kernel
Components used by more than one module may live in a Shared kernel (Shared/ or equivalent), mirroring the layered layout of a module — but only when they are truly universal technical/kernel concepts, not bounded-context business rules.
Promote-to-Shared rule (mandatory)
| Situation | Where it lives |
|---|---|
| Technical capability used by only one module | Keep it inside that module: Domain port (if needed) + Infrastructure adapter |
| The same technical capability is needed by two or more modules | Promote to Shared (port + adapter + bind in Shared composition root) |
| Cross-module business concept (member tier, stock line, money amount) | Never Shared — use ACL / Domain Events |
Start narrow; promote later. Do not invent Shared “just in case.”
Typical Shared candidates (after promotion)
DomainException/ base ensure helpersDatabaseTransactionInterfaceEventDispatcherInterfaceClockInterfaceConfigReaderInterface
These are technical. A “Money” value object that encodes Ordering pricing rules is not a kernel concept — keep it in the owning module or expose via ACL/Events.
Nuance — ubiquitous technical shapes
| Candidate | Shared? |
|---|---|
ClockInterface, transaction, event dispatch | Yes (after promote rule) |
| Opaque id type / UUID wrapper with no business rules | May promote if ≥2 modules need the same technical helper |
Money with currency plus Ordering tax/pricing policy | No — business; keep in owning BC |
| “MemberTier” enum used by Ordering and Directory | No — ACL / Events |
When in doubt: if removing the type would change a business invariant, it is not Shared.
Quick gate
Layout sketch
Shared/
├── Domain/
│ ├── Exceptions/
│ └── Ports/ # ClockInterface, EventDispatcherInterface, …
├── Application/
│ └── Providers/ # bind Shared ports
└── Infrastructure/
└── … # framework adapters for Shared ports
Host projects may place Shared under app/Shared or src/Shared. The promote rule does not change.
Next: decision tree.