ADR-026: Database enforces only structural integrity; foreign keys, CHECK constraints, and business rules live in the application
Context
The platform is built for extraction-readiness (ADR-020: domain boundary contracts across monolith↔microservice; SPEC-045 internal gRPC transport). Foreign keys bind tables into a single physical database and cannot span services, schemas, or shards — a domain that is FK-linked to another domain's tables cannot be extracted or re-sharded without a migration to break the constraint first. Cross-domain FKs were therefore already banned under the "opaque references, no cross-schema FK" rule (ADR-020). CHECK constraints and enum/value rules duplicate logic that the owning domain service already expresses in code (sealed types, validators), creating two sources of truth that must be kept in sync across migrations and deployments.
Domain writes already go through single-writer domain services, using pg_advisory_xact_lock where
concurrency matters, plus before-image audit companions and soft-delete on every mutable table —
the application, not the schema, is already the integrity authority for these domains.
ADR-017 (Application-Layer Enforcement of Business Constraints, 2026-05-18) established this
direction for the entitlement domain but left a carve-out: CHECK constraints for "physical
invariants" that are always true regardless of business rules (e.g. credits > 0,
credit_balance >= 0). The persona domain (PRs #618/#619/#620) went further and adopted a
zero-FK, zero-CHECK schema outright — PK/UNIQUE/index/NOT NULL only, with status enums and all
value validation living in the service layer, identity carried by account_handle, and mutable
attributes deliberately given no uniqueness constraint they shouldn't have. Persona's schema has
proven out cleanly against the write model above, with no CHECK constraint doing work the domain
service wasn't already doing.
Decision
The database enforces ONLY structural integrity: PRIMARY KEY, UNIQUE constraints, indexes,
and NOT NULL. No foreign keys. No CHECK constraints — including the "physical invariant"
carve-out ADR-017 permitted; that carve-out is retired for new schema. All referential integrity
(a child row's parent-id actually matching a live parent), enum/value validation, and business-rule
invariants (arithmetic, mutual exclusivity, state-transition legality) are enforced in the
application layer — in domain services, under advisory locks where concurrent writers require it.
NOT NULL is retained because it is column shape (a value must be present), not a business rule
about what that value may be.
Persona is the reference implementation of this ADR going forward.
Alternatives considered
| Option | Pros | Cons |
|---|---|---|
| Chosen: structural-only schema (PK/UNIQUE/index/NOT NULL); no FK, no CHECK | Matches the single-writer + advisory-lock write model; no insert-ordering coupling; soft-delete-friendly (no FK/partial-unique fights); extraction/sharding stays a code change, not a migration; one enforcement layer instead of two | The DB's last-line-of-defense against a buggy write, a bad migration, or a manual data fix is gone; correctness now rests entirely on the application (single-writer services, advisory locks, tests) |
| ADR-017's carve-out: allow CHECK for "physical invariants" only | Cheap guard against arithmetic bugs (negative balances, zero-day expiry) | Still a second place invariants can live; the line between "physical" and "business" invariant is judgment-prone and has already drifted domain to domain; persona shows it isn't needed even for physical invariants when the service owns all writes |
| Full DB-level enforcement (FK + CHECK + ENUM) | Hard safety net independent of application bugs | Directly contradicts extraction-readiness (FKs can't span services/shards); couples schema migrations to business-rule changes; blocks zero-downtime status additions; two sources of truth |
Consequences
What becomes easier
- Extraction and sharding stay pure application/deployment changes — no FK to break first.
- No insert-ordering coupling between domains or within a domain's own tables.
- Soft-delete and reconciliation-style writes stop fighting FK/partial-unique constraints.
- Cross-domain references stay opaque handles (already required by ADR-020) with nothing in the schema pretending otherwise.
- One enforcement layer to reason about and test, not two that can silently diverge.
What becomes harder
- The database can no longer catch a buggy write, a bad migration, or a manual hotfix that produces an orphaned reference, an invalid status value, or a negative balance — that entirely becomes the application's job (single-writer domain services, advisory locks, and test coverage). This is a conscious trade-off, acceptable given the write model already in place, not a gap being discovered after the fact.
- Existing domains (secret, compass, policy, organization, entitlement, etc.) still contain legacy FKs and CHECK constraints, including the ADR-017 physical-invariant CHECKs. These are grandfathered — reconciled opportunistically as those domains are touched, not via a blocking migration sweep. All new schema MUST comply with this ADR, including new tables added to an otherwise-grandfathered domain.
Risks
- Without a lint gate, a new migration can reintroduce a FK or CHECK by habit before a reviewer
catches it. Recommended follow-up (not built by this ADR): a migration lint that rejects new
FOREIGN KEY/CHECKclauses, mirroring the jOOQ-drift pre-push guard (tools/scripts/check-jooq-drift.sh) so this is enforced mechanically rather than by review vigilance alone.
References
- ADR-017 (Application-Layer Enforcement of Business Constraints) — this ADR tightens ADR-017 for new schema by retiring its physical-invariant CHECK carve-out; ADR-017's existing entitlement CHECKs are grandfathered under "existing domains" above.
- ADR-020 (domain boundary contracts) / ADR-021 (API URL scheme & gateway auth) — the domain boundary rules this ADR is downstream of (opaque cross-domain references, no cross-schema FK).
- SPEC-045 (internal gRPC transport) — the extraction path this ADR keeps unblocked.
- SPEC-070 (universal audit) — the before-image audit companion that, alongside single-writer domain services and advisory locks, is part of why the application can carry full integrity responsibility.
- Persona domain PRs #618, #619, #620 — reference implementation (no FKs, no CHECKs; PK/UNIQUE/
index/NOT NULL only; identity by
account_handle).