0015 — Domain-scoped outbox and table prefixes
Each domain owns its outbox, event types, and table prefix inside the service database.
Decided. Event delivery infrastructure and domain state live in per-domain
tables, not one service-wide events / event_types / commands set. Every table
that belongs to a domain is named with that domain's slug as a prefix. The service
database holds only cross-domain platform tables (principals and related auth
projections) plus one prefixed table group per domain.
Alternatives
- One outbox per service (previous layout). Simpler migrations early on, but
blurs domain ownership —
events,event_types, andcommandslook like platform tables when they are domain concerns. A service with chart and ledger domains would share one outbox and one idempotency namespace. - Schema per domain (
widget.events,order.events). Same isolation, but Postgres schema-per-tenant patterns add search-path and tooling friction we do not need inside one service database. - Unprefixed domain tables (
widgets,items). No consistent rule for spotting which domain owns a table or where outbox rows belong.
Table naming
Each domain declares a domain slug: short, lowercase, snake_case (order,
widget, chart, ledger).
| Rule | Example (domain order) |
|---|---|
| Root entity table is named exactly the domain slug | order |
Every other domain table starts with <slug>_ | order_item, order_line |
| Outbox and command idempotency use the same prefix | order_events, order_event_types, order_commands |
Service-scoped tables (no domain prefix): principals, principal_roles,
principal_groups, and any future tables that exist only for HTTP/process wiring
shared across domains in that service.
Outbox per domain
Each domain's outbox is its own <domain>_events table. The publisher drains every
domain outbox in the service. Broker topology is one fanout per domain; see
0016.
Why
- Ownership matches structure. Chart, ledger, and widget domains in one service each carry their own outbox, event-type registry, and idempotency namespace.
- Table names document the boundary. Prefixes make code review and SQL inspection show which domain owns a migration without reading comments.
- Same transactional outbox. This does not change 0004; it scopes the table to the domain that emits the events.
Consequences
- Adding a domain adds a migration bundle (
<domain>,<domain>_events, …), a publisher drain target, and a feed route — not rows in shared platform tables. - Event types are registered per domain, so cross-domain name collisions inside one service database are impossible.
- The template service implements this layout with
domain1anddomain2sample domains. 0004remains binding for outbox semantics; the "oneeventstable per service" wording there is superseded by this ADR for table placement and naming.
Canonical source: docs/decisions/0015-domain-scoped-outbox-and-table-prefixes.md.