Reads and reporting
Write-side replaceability is wasted if reports join every module’s tables through ORM relations. Treat cross-context reads as deliberately designed contracts.
Related: Ports & persistence · ACL · Events.
Ownership
| Data | Owner |
|---|---|
| Authoritative write model | The bounded-context module that defines the invariant |
| Cross-module foreign keys for business joins | Avoid as a coupling mechanism (Core FR-6) |
| Denormalised read model / projection | The module (or host reporting app) that needs the screen — fed by Events or sync ACL |
Patterns (allowed)
1. Sync ACL read
UI → Use Case → local read port → ACL → peer façade method that returns a small DTO (not Entities, not ORM).
Use for: eligibility checks, availability, “display peer label for this code.”
2. Event-sourced projection (preferred for lists/dashboards)
Publisher emits rich events → consumer/reporting Infrastructure translation listener → updates a local read table owned by the reader.
Use for: warehouse “open orders” board, directory “member activity” summaries.
3. Host reporting composition (outer layer)
A reporting UI/module in Infrastructure/UI may call multiple ACL façades or read replicas as an outer composer — still without putting foreign Domain types into another module’s Application/Domain.
Do not hide that composition inside a random Domain Entity.
Patterns (forbidden)
| Smell | Fix |
|---|---|
| ORM associations / joins across module namespaces for policy | ACL or projection |
| Shared “ReportModels” with everyone’s columns | Owned projection per consumer |
| Use Case running raw SQL across peer schemas | Peer façade / projection |
Practical guidance
- Screens that need one peer field → ACL read or denormalise a code + label via Event.
- Screens that need many rows from peers → projection updated asynchronously.
- Analytics warehouses / BI → export from each module or from projections; out of Core scope, but still not an excuse for Domain coupling.