Implements the Stable and Experimental JPA persistence platform designs against real PostgreSQL, adapted to this repository's fail-closed 19-leaf registry. The design models the platform as 25 Gradle projects. `src/settings.gradle` throws unless the registry holds exactly 19 leaves, so the plan's modules become packages inside `:adapter:outbound:persistence-jpa` (starter in `:app-bootstrap`, testkit in its own source set). The full mapping, the renames this repository's naming gate required, and every deliberate substitution are recorded in `docs/jpa/repository-adaptation.md`. Seven Docker-backed lanes replace the plan's seven JVM test suites. Each fails closed: a lane that discovers nothing, or a container that cannot start, is an error rather than a skip. Three defects the contracts found against a real server: - `CommitFailureClassifier` treated only SQLSTATE 40003, class 08, and transport breaks as completion-unknown. A backend terminated mid-commit reports 57P01, and the commit record may already be in the WAL — so a possibly-committed transaction could be re-run. 57P01/57P02/57P03 now classify as completion-unknown. - `SchemaTenantMigrationOrchestrator` recorded `MigrateResult`'s target version, which is empty for a tenant already current, reporting migrated tenants as unmigrated during a partial rollout. It now reads the applied version back from the tenant's schema history. - `JpaStreamExecutor` checked only the declared return type for reactive publishers, and `RegisteredPostgreSqlCopyLoader` passed the COPY timeout to `SET`, which is parsed before parameter binding. `JpaModuleBoundaryTest` enforces the plan's module map as package rules; `verifyCleanArchitectureDependencies` governs edges between leaves and cannot see these. Its first assertion is that the import is non-empty, because every rule under it is a `noClasses()` rule and would pass vacuously on an empty import. Verified: 128 container tests across all seven lanes, 1183 unit tests, `:adapter:outbound:persistence-jpa:check`, `:app-bootstrap:check`, `verifyCleanArchitectureDependencies`, `verifyOneTypePerFile`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1.9 KiB
ADR-JPA-003 — Completion unknown is never retried
- Status: Accepted
- Date: 2026-08-11
- Design: §17
Context
A connection can break while a commit is in flight. The server may have committed; the acknowledgement may simply have been lost. The driver cannot tell the two apart.
Decision
TransactionCompletionUnknownException is never retried, automatically or otherwise. It is
produced only by a failure observed while the transaction phase is COMMITTING, and only for
SQLSTATE 40003, a connection-class (08*) state, or a transport break. Recovery is
domain-specific reconciliation through TransactionCompletionResolver.
Consequences
Retrying a possibly-committed write is the most damaging thing this platform could do: a duplicate payment, a duplicate order, a double decrement. There is no budget or backoff that makes it safe, because the failure is epistemic rather than transient.
The invariant is enforced at the type level rather than by policy alone. JpaFailureContext refuses
to construct a retryable completion-unknown context, and the exception rebuilds its context through
the safe factory whatever it is handed. A future policy bug therefore cannot produce an unsafe
retry — the value it would need does not exist.
The rule is deliberately narrow in the other direction too. Classifying every connection failure as completion-unknown would push ordinary pool exhaustion and server restarts into the reconciliation queue, which trains operators to clear that queue without reading it — and then the one entry that mattered gets cleared with the rest.
The cost is that the domain must supply the resolver. The platform cannot: only the domain knows which idempotency record, business row, or outbox entry proves the write happened.
Enforcement
JpaFailureContextTest; DefaultJpaRetryPolicyTest; CommitFailureClassifierTest; release gate
completion-unknown-no-retry.