feat(jpa): implement the JPA relational persistence platform

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>
This commit is contained in:
DongHyeonka
2026-08-14 14:06:18 +09:00
co-authored by Claude Opus 5
parent 3b5aee50e3
commit 0e61f86eb5
401 changed files with 34504 additions and 197 deletions
@@ -3,6 +3,15 @@
// implementations, and the vendor-neutral SPI interfaces (OutboxClaimRepository /
// SqlStateErrorMapping). The PostgreSQL driver, flyway-database-postgresql dialect, and vendor
// Flyway migrations live only under the .postgresql subpackage (ArchUnit keeps the base neutral).
// The JPA relational persistence platform (docs/superpowers/specs/2026-08-11-jpa-persistence-
// platform-design.md) models itself as 18 Stable library modules. This repository's fail-closed
// 19-leaf registry outranks that layout, so those modules are packages here and
// JpaModuleBoundaryTest enforces the design's module dependency table. The full mapping is in
// docs/jpa/repository-adaptation.md.
//
// The testkit is its own source set rather than part of `test` because more than one lane consumes
// it and because a source set whose dependencies are declared only on the test configurations gives
// the design's "no production module depends on the testkit" guarantee without a new Gradle project.
sourceSets {
postgresqlIntegrationTest {
java.setSrcDirs(['src/postgresqlIntegrationTest/java'])
@@ -10,6 +19,16 @@ sourceSets {
compileClasspath += sourceSets.main.output
runtimeClasspath += output + compileClasspath
}
testkit {
java.srcDir 'src/testkit/java'
compileClasspath += sourceSets.main.output
runtimeClasspath += output + compileClasspath
}
jpaPlatformPerformanceTest {
java.srcDir 'src/jpaPlatformPerformanceTest/java'
compileClasspath += sourceSets.main.output + sourceSets.testkit.output
runtimeClasspath += output + compileClasspath
}
}
configurations {
@@ -17,6 +36,20 @@ configurations {
postgresqlIntegrationTestCompileOnly.extendsFrom testCompileOnly
postgresqlIntegrationTestRuntimeOnly.extendsFrom testRuntimeOnly
postgresqlIntegrationTestAnnotationProcessor.extendsFrom testAnnotationProcessor
testkitImplementation.extendsFrom testImplementation
testkitRuntimeOnly.extendsFrom testRuntimeOnly
jpaPlatformPerformanceTestImplementation.extendsFrom testImplementation
jpaPlatformPerformanceTestRuntimeOnly.extendsFrom testRuntimeOnly
}
// Every test lane compiles and runs against the testkit.
sourceSets.test {
compileClasspath += sourceSets.testkit.output
runtimeClasspath += sourceSets.testkit.output
}
sourceSets.postgresqlIntegrationTest {
compileClasspath += sourceSets.testkit.output
runtimeClasspath += sourceSets.testkit.output
}
ext.jpaPostgreSqlEvidenceImage = 'postgres:16-alpine'
@@ -43,9 +76,41 @@ dependencies {
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
// JPA platform observability (design §37). Micrometer's observation API already arrives with
// Spring; the meter registry does not, and the platform's transaction/query/retry metrics need
// it. Version managed by the Spring Boot BOM.
implementation 'io.micrometer:micrometer-core'
// Querydsl and Envers are Advanced opt-ins (design §4.2): the platform implements their
// contracts, but the Stable runtime classpath must not carry either. compileOnly keeps them off
// every deployment while still compiling the support classes; a deployment that opts in adds the
// artifact itself, and the guards refuse the capability when the classes are absent.
compileOnly 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
compileOnly 'org.hibernate.orm:hibernate-envers'
testImplementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
testImplementation 'org.hibernate.orm:hibernate-envers'
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
postgresqlIntegrationTestImplementation 'org.testcontainers:testcontainers-postgresql'
postgresqlIntegrationTestImplementation 'org.testcontainers:testcontainers-junit-jupiter'
postgresqlIntegrationTestImplementation 'org.testcontainers:testcontainers-toxiproxy'
postgresqlIntegrationTestRuntimeOnly 'org.postgresql:postgresql'
testkitImplementation 'org.testcontainers:testcontainers'
testkitImplementation 'org.testcontainers:testcontainers-postgresql'
testkitImplementation 'org.testcontainers:testcontainers-junit-jupiter'
testkitImplementation 'org.testcontainers:testcontainers-toxiproxy'
testkitImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
testkitRuntimeOnly 'org.postgresql:postgresql'
// The performance lane starts its own servers: pool saturation is only observable against a
// real database, because the thing being measured is what happens when every connection to it
// is already held.
jpaPlatformPerformanceTestImplementation 'org.testcontainers:testcontainers'
jpaPlatformPerformanceTestImplementation 'org.testcontainers:testcontainers-postgresql'
jpaPlatformPerformanceTestImplementation 'org.testcontainers:testcontainers-junit-jupiter'
jpaPlatformPerformanceTestRuntimeOnly 'org.postgresql:postgresql'
}
tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
@@ -169,4 +234,80 @@ postgresqlSecurityBaselineIntegrationTest.configure {
dependsOn project(':adapter:inbound:web').tasks.named('jpaPersistenceRedactionContractTest')
}
// JPA platform lanes (design §40-§41). Each maps one of the plan's JVM test suites onto this
// leaf's existing Docker-backed source set; the mapping is recorded in
// docs/jpa/repository-adaptation.md §3.
//
// Every lane fails closed. `failOnNoDiscoveredTests` matters more here than usual: a selected lane
// that discovers nothing reports success, and a contract suite that silently stopped running is
// indistinguishable from one that passes.
Closure<Void> registerJpaPlatformLane = { String taskName, String tag, String description ->
tasks.register(taskName, Test) {
group = 'verification'
it.description = description
testClassesDirs = sourceSets.postgresqlIntegrationTest.output.classesDirs
classpath = sourceSets.postgresqlIntegrationTest.runtimeClasspath
useJUnitPlatform {
includeTags tag
}
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs('-Duser.timezone=UTC')
// The Stable matrix selection. An unknown or empty value is an error in
// PostgreSqlVersion.parseSelection rather than an empty run.
systemProperty 'jpa.matrix.versions',
(project.findProperty('jpa.matrix.versions') ?: '16').toString()
}
}
def jpaPlatformContractTest = registerJpaPlatformLane(
'jpaPlatformContractTest',
'jpa-contract',
'Runs the JPA platform contract suite against real PostgreSQL (design §40).')
def jpaPlatformMigrationTest = registerJpaPlatformLane(
'jpaPlatformMigrationTest',
'jpa-migration',
'Runs the Flyway upgrade snapshot scenarios (design §31).')
def jpaPlatformFailureTest = registerJpaPlatformLane(
'jpaPlatformFailureTest',
'jpa-failure',
'Reproduces deadlock, serialization, and commit-ambiguity failures (design §39).')
def jpaPlatformQueryPlanTest = registerJpaPlatformLane(
'jpaPlatformQueryPlanTest',
'jpa-queryplan',
'Asserts query plan structure and planner estimate error (design §33).')
def jpaPlatformSecurityTest = registerJpaPlatformLane(
'jpaPlatformSecurityTest',
'jpa-security',
'Verifies runtime role privileges and search_path safety (design §36).')
// Machine-dependent bounds live in their own source set and never gate an ordinary build: attaching
// them to `check` would make a laptop's `check` fail for reasons that are not about the code.
def jpaPlatformPerformanceTest = tasks.register('jpaPlatformPerformanceTest', Test) {
group = 'verification'
description = 'Certifies Hikari pool and REQUIRES_NEW connection pressure (design §38).'
testClassesDirs = sourceSets.jpaPlatformPerformanceTest.output.classesDirs
classpath = sourceSets.jpaPlatformPerformanceTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs('-Duser.timezone=UTC')
systemProperty 'performance.assertions.enabled',
(project.findProperty('performance.assertions.enabled') ?: 'false').toString()
}
// The JPA release gate (design §41). Aggregates every lane whose absence would let one of the
// documented gates in docs/jpa/support-matrix.md pass unverified.
tasks.register('jpaPlatformReleaseGate') {
group = 'verification'
description = 'Runs every JPA platform lane required for a release (design §41).'
dependsOn tasks.named('test')
dependsOn jpaPlatformContractTest
dependsOn jpaPlatformMigrationTest
dependsOn jpaPlatformFailureTest
dependsOn jpaPlatformQueryPlanTest
dependsOn jpaPlatformSecurityTest
dependsOn jpaPlatformPerformanceTest
}
apply from: rootProject.file('gradle/jpa-evidence.gradle')