On this page
ADR-0002: Vertical domain crates, ports and the event outbox
Status: Accepted (plan decision D2). Implemented, with the deviations listed below.
Context
Domains are built in parallel by different engineers and must be testable on their own. Direct calls between domain crates would create a dependency web in which every change ripples everywhere and cycles become likely.
Decision
- Each domain is one crate, layered inside:
domain/(pure types and algorithms, no I/O) →repo/(SQL) →service/(application services, the only layer that opens transactions and checks permissions) →http/(handlers with no business logic). - Domain crates never depend on each other. Synchronous cross-domain reads go through
read-only ports (traits) declared in
crates/commonlayer-core/src/ports/and implemented by the providing crate; the composition root wires them. - Cross-domain reactions go through the event outbox: a service writes a typed event
to
domain_eventsin its own transaction; a dispatcher job fans it out to subscriber jobs. - A crate writes only the tables it owns.
Consequences
- Every domain has its own integration tests against real PostgreSQL with fakes for
the ports it consumes (
crates/commonlayer-testkit/src/fakes/). - Ports grew additively during implementation (for example
MaintainerDirectory::payee_readiness,SecurityPosture::retention_protection,ArtifactCatalog::inventory); new methods carry default implementations so that providers and consumers could move independently. - Two outbound adapter traits (
RegistryGateway,SignatureVerifier) also live inports::artifacts, although ports were meant to be read- or enqueue-only, so that the artifacts and registry crates stay independent. - Deviations recorded in the status files: the projects crate inserts
usersrows for project service accounts (no port can create users; WP-10); the security crate keeps its ownsecurity_digest_indexbecauseArtifactCataloghas no lookup by digest value (WP-12); domain crates depend oncommonlayer-observability(ADR-0018).