# JPA Experimental Expansion Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Stable JPA 플랫폼을 변경하지 않고 Multi-tenancy, PostgreSQL RLS, schema/database tenant 분리, consistency-aware Read Replica, Jakarta Persistence 4.0, Hibernate ORM 8, PostgreSQL 19 호환성을 독립 Experimental 모듈과 승격 Gate로 검증한다. **Architecture:** Experimental module은 Stable `jpa-core-api` 계약만 소비하며 Stable starter에 자동 포함되지 않는다. 각 기능은 명시적 feature flag와 별도 compatibility/failure suite를 요구한다. 실험 결과가 Stable 의미론과 충돌하면 Core를 왜곡하지 않고 capability 또는 별도 profile로 유지한다. **Tech Stack:** Stable 계획의 Java 21·Spring Boot 4.1·PostgreSQL Testcontainers 기반, PostgreSQL RLS, AbstractRoutingDataSource, tenant-specific DataSource registry, Jakarta Persistence 4.0 preview/final compatibility lane, Hibernate ORM 8 compatibility lane, PostgreSQL 19 compatibility lane. ## Global Constraints - Stable 계획 Task 1~53이 완료되고 Release Gate가 통과한 뒤 시작한다. - 모듈 루트는 `modules/jpa-experimental`이다. - Experimental module은 `jpa-spring-boot-starter`의 기본 dependency가 아니다. - 모든 기능은 `backend.jpa.experimental.*` feature flag를 요구한다. - Tenant ID와 consistency token은 metric label에 기록하지 않는다. - Tenant context 누락은 fail-closed다. - `readOnly=true`만으로 replica routing하지 않는다. - Lock query, write transaction, read-after-write pin은 primary를 사용한다. - JPA4/Hibernate8/PG19 결과로 Stable 3.2/7.4/PG16~18 contract를 수정하지 않는다. - 승격 전 별도 security, failure, migration and compatibility evidence가 필요하다. --- ## 1. Experimental 파일 구조 ```text modules/jpa-experimental/ ├── jpa-experimental-core/ ├── jpa-multitenancy-column/ ├── jpa-multitenancy-rls/ ├── jpa-multitenancy-schema/ ├── jpa-multitenancy-database/ ├── jpa-read-replica/ └── jpa-next-compatibility/ ``` --- ### Task 1: Experimental Module·Feature Gate·Dependency Isolation 구성 **Files:** - Create: `modules/jpa-experimental/jpa-experimental-core/build.gradle.kts` - Create: `modules/jpa-experimental/jpa-multitenancy-column/build.gradle.kts` - Create: `modules/jpa-experimental/jpa-multitenancy-rls/build.gradle.kts` - Create: `modules/jpa-experimental/jpa-multitenancy-schema/build.gradle.kts` - Create: `modules/jpa-experimental/jpa-multitenancy-database/build.gradle.kts` - Create: `modules/jpa-experimental/jpa-read-replica/build.gradle.kts` - Create: `modules/jpa-experimental/jpa-next-compatibility/build.gradle.kts` - Create: `modules/jpa-experimental/jpa-experimental-core/src/main/java/io/backend/skeleton/jpa/experimental/ExperimentalFeature.java` - Create: `modules/jpa-experimental/jpa-experimental-core/src/main/java/io/backend/skeleton/jpa/experimental/ExperimentalFeatureGate.java` - Modify: `settings.gradle.kts` - Test: `modules/jpa-experimental/jpa-experimental-core/src/test/java/io/backend/skeleton/jpa/experimental/ExperimentalFeatureGateTest.java` **Interfaces:** - Consumes: Stable `jpa-core-api` and explicit environment feature flags. - Produces: Isolated experimental projects that cannot enter the Stable starter transitively. **Implementation requirements:** - Every module depends only on Stable public contracts, never on Stable internal packages. - Feature gate fails startup when module is present but flag is absent. - Add a dependency graph test proving the Stable starter has no experimental dependency. - [ ] **Step 1: Write the failing test** ```java package io.backend.skeleton.jpa.experimental; class ExperimentalFeatureGateTest { @Test void featureIsDisabledUnlessExplicitlyEnabled() { assertThatThrownBy(() -> gate.requireEnabled(MULTITENANCY_COLUMN, Map.of())) .hasMessageContaining("backend.jpa.experimental.multitenancy-column=true"); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-experimental-core:test --tests 'io.backend.skeleton.jpa.experimental.ExperimentalFeatureGateTest' ``` Expected: FAIL because the production type or behavior does not exist yet. - [ ] **Step 3: Implement the smallest complete production contract** ```java package io.backend.skeleton.jpa.experimental; public final class ExperimentalFeatureGate { public void requireEnabled( ExperimentalFeature feature, Map flags) { if (!Boolean.TRUE.equals(flags.get(feature.property()))) { throw new IllegalStateException(feature.property() + "=true is required"); } } } ``` Implement every file and invariant listed under **Implementation requirements**; the snippet fixes the public names and central behavior rather than replacing those requirements. - [ ] **Step 4: Run the focused test and the module test suite** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-experimental-core:test --tests 'io.backend.skeleton.jpa.experimental.ExperimentalFeatureGateTest' ./gradlew :modules:jpa-experimental:jpa-experimental-core:test ``` Expected: PASS with all assertions green. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/jpa-experimental/jpa-experimental-core/build.gradle.kts' 'modules/jpa-experimental/jpa-multitenancy-column/build.gradle.kts' 'modules/jpa-experimental/jpa-multitenancy-rls/build.gradle.kts' 'modules/jpa-experimental/jpa-multitenancy-schema/build.gradle.kts' 'modules/jpa-experimental/jpa-multitenancy-database/build.gradle.kts' 'modules/jpa-experimental/jpa-read-replica/build.gradle.kts' 'modules/jpa-experimental/jpa-next-compatibility/build.gradle.kts' 'modules/jpa-experimental/jpa-experimental-core/src/main/java/io/backend/skeleton/jpa/experimental/ExperimentalFeature.java' 'modules/jpa-experimental/jpa-experimental-core/src/main/java/io/backend/skeleton/jpa/experimental/ExperimentalFeatureGate.java' 'settings.gradle.kts' 'modules/jpa-experimental/jpa-experimental-core/src/test/java/io/backend/skeleton/jpa/experimental/ExperimentalFeatureGateTest.java' git commit -m "build: isolate jpa experimental modules" ``` ### Task 2: Shared-schema Tenant Context와 Column Guard 구현 **Files:** - Create: `modules/jpa-experimental/jpa-multitenancy-column/src/main/java/io/backend/skeleton/jpa/experimental/tenant/TenantId.java` - Create: `modules/jpa-experimental/jpa-multitenancy-column/src/main/java/io/backend/skeleton/jpa/experimental/tenant/TenantContext.java` - Create: `modules/jpa-experimental/jpa-multitenancy-column/src/main/java/io/backend/skeleton/jpa/experimental/tenant/TenantAwareRepositoryGuard.java` - Create: `modules/jpa-experimental/jpa-multitenancy-column/src/main/java/io/backend/skeleton/jpa/experimental/tenant/TenantEntityListenerGuard.java` - Test: `modules/jpa-experimental/jpa-multitenancy-column/src/integrationTest/java/io/backend/skeleton/jpa/experimental/tenant/TenantColumnIsolationTest.java` **Interfaces:** - Consumes: Explicit request/job tenant context and domain Entity tenant-column contracts. - Produces: Fail-closed tenant context propagation and query/write isolation evidence. **Implementation requirements:** - Reject Repository access when tenant context is absent outside an audited admin scope. - Require tenant column in unique/index requirements where isolation depends on it. - Test async job context propagation and cleanup. - Do not rely on Hibernate filter alone as the final security boundary. - [ ] **Step 1: Write the failing test** ```java package io.backend.skeleton.jpa.experimental.tenant; class TenantColumnIsolationTest { @Test void tenantARepositoryCannotReadTenantBRows() { insertFor(TENANT_A, "a"); insertFor(TENANT_B, "b"); assertThat(withTenant(TENANT_A, repository::findAll)) .extracting(Item::value) .containsExactly("a"); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-multitenancy-column:integrationTest --tests 'io.backend.skeleton.jpa.experimental.tenant.TenantColumnIsolationTest' ``` Expected: FAIL because the production type or behavior does not exist yet. - [ ] **Step 3: Implement the smallest complete production contract** ```java package io.backend.skeleton.jpa.experimental.tenant; public final class TenantContext { private static final ThreadLocal CURRENT = new ThreadLocal<>(); public static TenantId require() { TenantId tenant = CURRENT.get(); if (tenant == null) throw new IllegalStateException("tenant context is required"); return tenant; } public static void clear() { CURRENT.remove(); } } ``` Implement every file and invariant listed under **Implementation requirements**; the snippet fixes the public names and central behavior rather than replacing those requirements. - [ ] **Step 4: Run the focused test and the module test suite** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-multitenancy-column:integrationTest --tests 'io.backend.skeleton.jpa.experimental.tenant.TenantColumnIsolationTest' ./gradlew :modules:jpa-experimental:jpa-multitenancy-column:test ``` Expected: PASS with all assertions green. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/jpa-experimental/jpa-multitenancy-column/src/main/java/io/backend/skeleton/jpa/experimental/tenant/TenantId.java' 'modules/jpa-experimental/jpa-multitenancy-column/src/main/java/io/backend/skeleton/jpa/experimental/tenant/TenantContext.java' 'modules/jpa-experimental/jpa-multitenancy-column/src/main/java/io/backend/skeleton/jpa/experimental/tenant/TenantAwareRepositoryGuard.java' 'modules/jpa-experimental/jpa-multitenancy-column/src/main/java/io/backend/skeleton/jpa/experimental/tenant/TenantEntityListenerGuard.java' 'modules/jpa-experimental/jpa-multitenancy-column/src/integrationTest/java/io/backend/skeleton/jpa/experimental/tenant/TenantColumnIsolationTest.java' git commit -m "feat: add experimental tenant column isolation" ``` ### Task 3: PostgreSQL RLS Tenant Policy와 Connection Reuse Guard 구현 **Files:** - Create: `modules/jpa-experimental/jpa-multitenancy-rls/src/main/java/io/backend/skeleton/jpa/experimental/rls/RlsTenantSessionBinder.java` - Create: `modules/jpa-experimental/jpa-multitenancy-rls/src/main/java/io/backend/skeleton/jpa/experimental/rls/RlsPolicyVerifier.java` - Create: `modules/jpa-experimental/jpa-multitenancy-rls/src/main/java/io/backend/skeleton/jpa/experimental/rls/RlsAdminBypassToken.java` - Create: `modules/jpa-experimental/jpa-multitenancy-rls/src/main/resources/db/experimental-rls/V1__tenant_rls.sql` - Test: `modules/jpa-experimental/jpa-multitenancy-rls/src/failureTest/java/io/backend/skeleton/jpa/experimental/rls/RlsIsolationFailureTest.java` **Interfaces:** - Consumes: TenantContext, PostgreSQL transaction-local settings and restricted runtime role. - Produces: Database-enforced tenant isolation that resets safely across pooled connections. **Implementation requirements:** - Set tenant context with transaction-local `set_config` before tenant queries. - Prove a pooled connection cannot leak the prior tenant into the next transaction. - Runtime role must not own tables or bypass RLS. - Admin bypass requires a separate DataSource and audit token. - [ ] **Step 1: Write the failing test** ```java package io.backend.skeleton.jpa.experimental.rls; class RlsIsolationFailureTest { @Test void pooledConnectionDoesNotLeakPriorTenantSetting() { withTenant(TENANT_A, () -> assertThat(repository.count()).isEqualTo(1)); withTenant(TENANT_B, () -> assertThat(repository.count()).isEqualTo(1)); withoutTenant(() -> assertThatThrownBy(repository::count).isInstanceOf(DataAccessException.class)); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-multitenancy-rls:failureTest --tests 'io.backend.skeleton.jpa.experimental.rls.RlsIsolationFailureTest' ``` Expected: FAIL because the production type or behavior does not exist yet. - [ ] **Step 3: Implement the smallest complete production contract** ```java package io.backend.skeleton.jpa.experimental.rls; public final class RlsTenantSessionBinder { public void bind(EntityManager entityManager, TenantId tenant) { entityManager.createNativeQuery( "select set_config('app.tenant_id', :tenant, true)") .setParameter("tenant", tenant.value()) .getSingleResult(); } } ``` Implement every file and invariant listed under **Implementation requirements**; the snippet fixes the public names and central behavior rather than replacing those requirements. - [ ] **Step 4: Run the focused test and the module test suite** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-multitenancy-rls:failureTest --tests 'io.backend.skeleton.jpa.experimental.rls.RlsIsolationFailureTest' ./gradlew :modules:jpa-experimental:jpa-multitenancy-rls:test ``` Expected: PASS with all assertions green. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/jpa-experimental/jpa-multitenancy-rls/src/main/java/io/backend/skeleton/jpa/experimental/rls/RlsTenantSessionBinder.java' 'modules/jpa-experimental/jpa-multitenancy-rls/src/main/java/io/backend/skeleton/jpa/experimental/rls/RlsPolicyVerifier.java' 'modules/jpa-experimental/jpa-multitenancy-rls/src/main/java/io/backend/skeleton/jpa/experimental/rls/RlsAdminBypassToken.java' 'modules/jpa-experimental/jpa-multitenancy-rls/src/main/resources/db/experimental-rls/V1__tenant_rls.sql' 'modules/jpa-experimental/jpa-multitenancy-rls/src/failureTest/java/io/backend/skeleton/jpa/experimental/rls/RlsIsolationFailureTest.java' git commit -m "feat: add experimental postgresql rls isolation" ``` ### Task 4: Schema-per-tenant Connection Provider와 Migration Orchestrator 구현 **Files:** - Create: `modules/jpa-experimental/jpa-multitenancy-schema/src/main/java/io/backend/skeleton/jpa/experimental/schema/SchemaTenantRegistry.java` - Create: `modules/jpa-experimental/jpa-multitenancy-schema/src/main/java/io/backend/skeleton/jpa/experimental/schema/SchemaMultiTenantConnectionProvider.java` - Create: `modules/jpa-experimental/jpa-multitenancy-schema/src/main/java/io/backend/skeleton/jpa/experimental/schema/SchemaTenantMigrationOrchestrator.java` - Test: `modules/jpa-experimental/jpa-multitenancy-schema/src/migrationTest/java/io/backend/skeleton/jpa/experimental/schema/SchemaTenantMigrationContractTest.java` **Interfaces:** - Consumes: Validated tenant→schema catalog and Flyway migration gate. - Produces: Bounded schema selection and per-tenant migration status without accepting raw schema names. **Implementation requirements:** - Map TenantId to a pre-registered schema identifier; no user-provided SQL identifier. - Reset schema/search_path when returning pooled connections. - Track migration version and failure per tenant. - Rate-limit tenant migrations and support resume without auto-repair. - [ ] **Step 1: Write the failing test** ```java package io.backend.skeleton.jpa.experimental.schema; class SchemaTenantMigrationContractTest { @Test void migratesOnlyRegisteredSchemasAndResumesAfterFailure() { orchestrator.migrateAll(List.of(TENANT_A, TENANT_B)); assertThat(status(TENANT_A).version()).isEqualTo(LATEST); assertThatThrownBy(() -> orchestrator.migrate(new TenantId("../public"))) .isInstanceOf(IllegalArgumentException.class); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-multitenancy-schema:migrationTest --tests 'io.backend.skeleton.jpa.experimental.schema.SchemaTenantMigrationContractTest' ``` Expected: FAIL because the production type or behavior does not exist yet. - [ ] **Step 3: Implement the smallest complete production contract** ```java package io.backend.skeleton.jpa.experimental.schema; public final class SchemaTenantRegistry { public String requireSchema(TenantId tenant) { return Optional.ofNullable(schemaByTenant.get(tenant)) .orElseThrow(() -> new IllegalArgumentException("unregistered tenant schema")); } } ``` Implement every file and invariant listed under **Implementation requirements**; the snippet fixes the public names and central behavior rather than replacing those requirements. - [ ] **Step 4: Run the focused test and the module test suite** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-multitenancy-schema:migrationTest --tests 'io.backend.skeleton.jpa.experimental.schema.SchemaTenantMigrationContractTest' ./gradlew :modules:jpa-experimental:jpa-multitenancy-schema:test ``` Expected: PASS with all assertions green. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/jpa-experimental/jpa-multitenancy-schema/src/main/java/io/backend/skeleton/jpa/experimental/schema/SchemaTenantRegistry.java' 'modules/jpa-experimental/jpa-multitenancy-schema/src/main/java/io/backend/skeleton/jpa/experimental/schema/SchemaMultiTenantConnectionProvider.java' 'modules/jpa-experimental/jpa-multitenancy-schema/src/main/java/io/backend/skeleton/jpa/experimental/schema/SchemaTenantMigrationOrchestrator.java' 'modules/jpa-experimental/jpa-multitenancy-schema/src/migrationTest/java/io/backend/skeleton/jpa/experimental/schema/SchemaTenantMigrationContractTest.java' git commit -m "feat: add experimental schema per tenant persistence" ``` ### Task 5: Database-per-tenant DataSource Registry와 Capacity Guard 구현 **Files:** - Create: `modules/jpa-experimental/jpa-multitenancy-database/src/main/java/io/backend/skeleton/jpa/experimental/database/TenantDataSourceRegistry.java` - Create: `modules/jpa-experimental/jpa-multitenancy-database/src/main/java/io/backend/skeleton/jpa/experimental/database/TenantEntityManagerFactoryRegistry.java` - Create: `modules/jpa-experimental/jpa-multitenancy-database/src/main/java/io/backend/skeleton/jpa/experimental/database/TenantPoolBudget.java` - Create: `modules/jpa-experimental/jpa-multitenancy-database/src/main/java/io/backend/skeleton/jpa/experimental/database/TenantDataSourceLifecycle.java` - Test: `modules/jpa-experimental/jpa-multitenancy-database/src/performanceTest/java/io/backend/skeleton/jpa/experimental/database/TenantPoolCapacityContractTest.java` **Interfaces:** - Consumes: Secret-backed tenant connection profiles and global DB connection budget. - Produces: Lazy bounded per-tenant pools with eviction, credential rotation and migration status. **Implementation requirements:** - Never create an unbounded Hikari pool per tenant. - Enforce global maximum pools and connections before creating a DataSource. - Drain and close pools on tenant removal or credential rotation. - Do not expose tenant JDBC URLs or credentials in diagnostics. - [ ] **Step 1: Write the failing test** ```java package io.backend.skeleton.jpa.experimental.database; class TenantPoolCapacityContractTest { @Test void refusesNewTenantPoolWhenGlobalConnectionBudgetIsExhausted() { registry.openTenants(globalBudget().maxTenants()); assertThatThrownBy(() -> registry.require(ANOTHER_TENANT)) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("tenant pool budget"); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-multitenancy-database:performanceTest --tests 'io.backend.skeleton.jpa.experimental.database.TenantPoolCapacityContractTest' ``` Expected: FAIL because the production type or behavior does not exist yet. - [ ] **Step 3: Implement the smallest complete production contract** ```java package io.backend.skeleton.jpa.experimental.database; public record TenantPoolBudget( int maxOpenPools, int maxConnectionsAcrossPools) { public void requireCapacity(int openPools, int allocatedConnections) { if (openPools >= maxOpenPools || allocatedConnections >= maxConnectionsAcrossPools) { throw new IllegalStateException("tenant pool budget exhausted"); } } } ``` Implement every file and invariant listed under **Implementation requirements**; the snippet fixes the public names and central behavior rather than replacing those requirements. - [ ] **Step 4: Run the focused test and the module test suite** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-multitenancy-database:performanceTest --tests 'io.backend.skeleton.jpa.experimental.database.TenantPoolCapacityContractTest' ./gradlew :modules:jpa-experimental:jpa-multitenancy-database:test ``` Expected: PASS with all assertions green. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/jpa-experimental/jpa-multitenancy-database/src/main/java/io/backend/skeleton/jpa/experimental/database/TenantDataSourceRegistry.java' 'modules/jpa-experimental/jpa-multitenancy-database/src/main/java/io/backend/skeleton/jpa/experimental/database/TenantEntityManagerFactoryRegistry.java' 'modules/jpa-experimental/jpa-multitenancy-database/src/main/java/io/backend/skeleton/jpa/experimental/database/TenantPoolBudget.java' 'modules/jpa-experimental/jpa-multitenancy-database/src/main/java/io/backend/skeleton/jpa/experimental/database/TenantDataSourceLifecycle.java' 'modules/jpa-experimental/jpa-multitenancy-database/src/performanceTest/java/io/backend/skeleton/jpa/experimental/database/TenantPoolCapacityContractTest.java' git commit -m "feat: add experimental database per tenant registry" ``` ### Task 6: Consistency-aware Read Replica Routing 구현 **Files:** - Create: `modules/jpa-experimental/jpa-read-replica/src/main/java/io/backend/skeleton/jpa/experimental/replica/ReadConsistency.java` - Create: `modules/jpa-experimental/jpa-read-replica/src/main/java/io/backend/skeleton/jpa/experimental/replica/ConsistencyToken.java` - Create: `modules/jpa-experimental/jpa-read-replica/src/main/java/io/backend/skeleton/jpa/experimental/replica/ReplicaRoutingDecision.java` - Create: `modules/jpa-experimental/jpa-read-replica/src/main/java/io/backend/skeleton/jpa/experimental/replica/ConsistencyAwareDataSourceRouter.java` - Create: `modules/jpa-experimental/jpa-read-replica/src/main/java/io/backend/skeleton/jpa/experimental/replica/ReplicaLagMonitor.java` - Test: `modules/jpa-experimental/jpa-read-replica/src/failureTest/java/io/backend/skeleton/jpa/experimental/replica/ReadAfterWriteRoutingContractTest.java` **Interfaces:** - Consumes: Primary/replica DataSources, transaction state, lock intent and replica lag evidence. - Produces: Routing decisions for PRIMARY_REQUIRED, BOUNDED_STALENESS and EVENTUAL reads. **Implementation requirements:** - Writes, lock queries, REQUIRES_NEW writes and active write transactions always use primary. - Read-after-write uses a consistency token or primary pin, not `readOnly=true` alone. - Fallback to primary when replica lag exceeds policy or evidence is unavailable. - Keep routing fixed for the life of one transaction. - [ ] **Step 1: Write the failing test** ```java package io.backend.skeleton.jpa.experimental.replica; class ReadAfterWriteRoutingContractTest { @Test void immediateReadAfterWriteUsesPrimaryUntilConsistencyTokenIsSatisfied() { var token = service.writeAndReturnConsistencyToken(); var decision = router.route(readOnlyTransaction(), ReadConsistency.after(token)); assertThat(decision.target()).isEqualTo(PRIMARY); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-read-replica:failureTest --tests 'io.backend.skeleton.jpa.experimental.replica.ReadAfterWriteRoutingContractTest' ``` Expected: FAIL because the production type or behavior does not exist yet. - [ ] **Step 3: Implement the smallest complete production contract** ```java package io.backend.skeleton.jpa.experimental.replica; public final class ConsistencyAwareDataSourceRouter { public ReplicaRoutingDecision route( TransactionContext transaction, ReadConsistency consistency) { if (transaction.write() || transaction.locking() || !lagMonitor.satisfies(consistency)) { return ReplicaRoutingDecision.primary(); } return ReplicaRoutingDecision.replica(); } } ``` Implement every file and invariant listed under **Implementation requirements**; the snippet fixes the public names and central behavior rather than replacing those requirements. - [ ] **Step 4: Run the focused test and the module test suite** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-read-replica:failureTest --tests 'io.backend.skeleton.jpa.experimental.replica.ReadAfterWriteRoutingContractTest' ./gradlew :modules:jpa-experimental:jpa-read-replica:test ``` Expected: PASS with all assertions green. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/jpa-experimental/jpa-read-replica/src/main/java/io/backend/skeleton/jpa/experimental/replica/ReadConsistency.java' 'modules/jpa-experimental/jpa-read-replica/src/main/java/io/backend/skeleton/jpa/experimental/replica/ConsistencyToken.java' 'modules/jpa-experimental/jpa-read-replica/src/main/java/io/backend/skeleton/jpa/experimental/replica/ReplicaRoutingDecision.java' 'modules/jpa-experimental/jpa-read-replica/src/main/java/io/backend/skeleton/jpa/experimental/replica/ConsistencyAwareDataSourceRouter.java' 'modules/jpa-experimental/jpa-read-replica/src/main/java/io/backend/skeleton/jpa/experimental/replica/ReplicaLagMonitor.java' 'modules/jpa-experimental/jpa-read-replica/src/failureTest/java/io/backend/skeleton/jpa/experimental/replica/ReadAfterWriteRoutingContractTest.java' git commit -m "feat: add experimental consistency aware replica routing" ``` ### Task 7: Jakarta Persistence 4.0 Compatibility Lane 구현 **Files:** - Create: `modules/jpa-experimental/jpa-next-compatibility/src/compatibilityJpa4/java/io/backend/skeleton/jpa/experimental/next/Jpa4CompatibilityTest.java` - Create: `.github/workflows/jpa-next-jpa4.yml` - Modify: `modules/jpa-experimental/jpa-next-compatibility/build.gradle.kts` - Test: `modules/jpa-experimental/jpa-next-compatibility/src/test/java/io/backend/skeleton/jpa/experimental/next/CompatibilityLaneDefinitionTest.java` **Interfaces:** - Consumes: Published Jakarta Persistence 4.0 milestone/final artifact when available and the Stable contract suite. - Produces: A non-blocking compatibility report that does not alter Stable JPA 3.2 APIs. **Implementation requirements:** - Run the Stable public API compilation and selected mapping contracts against JPA 4. - Record removed/changed APIs and provider support separately. - Do not publish JPA4 compiled artifacts under Stable coordinates. - [ ] **Step 1: Write the failing test** ```kotlin package io.backend.skeleton.jpa.experimental.next; class CompatibilityLaneDefinitionTest { @Test void jpaFourLaneIsExperimentalAndSeparateFromStablePublication() { assertThat(lane("jpa4").publicationEnabled()).isFalse(); assertThat(lane("jpa4").supportLevel()).isEqualTo(EXPERIMENTAL); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-next-compatibility:test --tests 'io.backend.skeleton.jpa.experimental.next.CompatibilityLaneDefinitionTest' ``` Expected: FAIL because the production type or behavior does not exist yet. - [ ] **Step 3: Implement the smallest complete production contract** ```kotlin testing { suites { register("compatibilityJpa4") { useJUnitJupiter() dependencies { implementation(project(":modules:jpa:jpa-core-api")) implementation(libs.jakarta.persistence.next) } } } } ``` Implement every file and invariant listed under **Implementation requirements**; the snippet fixes the public names and central behavior rather than replacing those requirements. - [ ] **Step 4: Run the focused test and the module test suite** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-next-compatibility:test --tests 'io.backend.skeleton.jpa.experimental.next.CompatibilityLaneDefinitionTest' ./gradlew :modules:jpa-experimental:jpa-next-compatibility:test ``` Expected: PASS with all assertions green. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/jpa-experimental/jpa-next-compatibility/src/compatibilityJpa4/java/io/backend/skeleton/jpa/experimental/next/Jpa4CompatibilityTest.java' '.github/workflows/jpa-next-jpa4.yml' 'modules/jpa-experimental/jpa-next-compatibility/build.gradle.kts' 'modules/jpa-experimental/jpa-next-compatibility/src/test/java/io/backend/skeleton/jpa/experimental/next/CompatibilityLaneDefinitionTest.java' git commit -m "test: add jakarta persistence four compatibility lane" ``` ### Task 8: Hibernate ORM 8 Compatibility Lane 구현 **Files:** - Create: `modules/jpa-experimental/jpa-next-compatibility/src/compatibilityHibernate8/java/io/backend/skeleton/jpa/experimental/next/Hibernate8CompatibilityTest.java` - Create: `.github/workflows/jpa-next-hibernate8.yml` - Modify: `modules/jpa-experimental/jpa-next-compatibility/build.gradle.kts` - Test: `modules/jpa-experimental/jpa-next-compatibility/src/test/java/io/backend/skeleton/jpa/experimental/next/HibernateCompatibilityPolicyTest.java` **Interfaces:** - Consumes: Hibernate ORM 8 milestone/final artifact and Stable Hibernate 7.4 regression suites. - Produces: Generated SQL, fetch pagination, statistics, batch and extension compatibility evidence. **Implementation requirements:** - Re-run collection fetch pagination, StatementInspector, Statistics, JSONB, Batch and StatelessSession contracts. - Record SQL and performance differences without weakening the 7.4 Stable gate. - Do not allow Hibernate 8 dependencies in Stable published modules. - [ ] **Step 1: Write the failing test** ```kotlin package io.backend.skeleton.jpa.experimental.next; class HibernateCompatibilityPolicyTest { @Test void hibernateEightCannotReplaceStableProviderWithoutPromotion() { assertThat(policy.stableProvider()).isEqualTo("7.4"); assertThat(policy.experimentalProviders()).contains("8"); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-next-compatibility:test --tests 'io.backend.skeleton.jpa.experimental.next.HibernateCompatibilityPolicyTest' ``` Expected: FAIL because the production type or behavior does not exist yet. - [ ] **Step 3: Implement the smallest complete production contract** ```kotlin testing { suites { register("compatibilityHibernate8") { useJUnitJupiter() dependencies { implementation(project(":modules:jpa:jpa-testkit-postgresql")) implementation(libs.hibernate.orm.next) } } } } ``` Implement every file and invariant listed under **Implementation requirements**; the snippet fixes the public names and central behavior rather than replacing those requirements. - [ ] **Step 4: Run the focused test and the module test suite** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-next-compatibility:test --tests 'io.backend.skeleton.jpa.experimental.next.HibernateCompatibilityPolicyTest' ./gradlew :modules:jpa-experimental:jpa-next-compatibility:test ``` Expected: PASS with all assertions green. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/jpa-experimental/jpa-next-compatibility/src/compatibilityHibernate8/java/io/backend/skeleton/jpa/experimental/next/Hibernate8CompatibilityTest.java' '.github/workflows/jpa-next-hibernate8.yml' 'modules/jpa-experimental/jpa-next-compatibility/build.gradle.kts' 'modules/jpa-experimental/jpa-next-compatibility/src/test/java/io/backend/skeleton/jpa/experimental/next/HibernateCompatibilityPolicyTest.java' git commit -m "test: add hibernate eight compatibility lane" ``` ### Task 9: PostgreSQL 19 Compatibility와 Stable 승격 Gate 구현 **Files:** - Create: `modules/jpa-experimental/jpa-next-compatibility/src/compatibilityPostgresql19/java/io/backend/skeleton/jpa/experimental/next/PostgreSql19CompatibilityTest.java` - Create: `docs/jpa/experimental-support-matrix.md` - Create: `docs/jpa/experimental-promotion-checklist.md` - Create: `.github/workflows/jpa-next-postgresql19.yml` - Modify: `modules/jpa-experimental/jpa-next-compatibility/build.gradle.kts` - Test: `modules/jpa-experimental/jpa-next-compatibility/src/test/java/io/backend/skeleton/jpa/experimental/next/ExperimentalPromotionGateTest.java` **Interfaces:** - Consumes: PG19 image when GA, all Stable contracts, experimental security/failure/migration/performance reports. - Produces: A promotion decision that requires evidence rather than version availability alone. **Implementation requirements:** - Run mapping, SQLSTATE, lock, batch, Flyway, plan and native extension contracts on PG19. - Promotion requires two supported patch runs and no unresolved semantic regression. - Multi-tenancy/replica promotion requires tenant leakage, failover, lag and pool-capacity evidence. - Update Stable support matrix only through a reviewed ADR. - [ ] **Step 1: Write the failing test** ```java package io.backend.skeleton.jpa.experimental.next; class ExperimentalPromotionGateTest { @Test void promotionRequiresAllEvidenceAndReviewedAdr() { var evidence = evidence().withCompatibility(true).withSecurity(true).withFailure(true) .withMigration(true).withPerformance(true).withReviewedAdr(false); assertThat(gate.evaluate(evidence)).isEqualTo(BLOCKED_MISSING_ADR); } } ``` - [ ] **Step 2: Run the focused test and verify the failure** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-next-compatibility:test --tests 'io.backend.skeleton.jpa.experimental.next.ExperimentalPromotionGateTest' ``` Expected: FAIL because the production type or behavior does not exist yet. - [ ] **Step 3: Implement the smallest complete production contract** ```java package io.backend.skeleton.jpa.experimental.next; public final class ExperimentalPromotionGate { public PromotionDecision evaluate(PromotionEvidence evidence) { if (!evidence.allTechnicalGatesPassed()) return BLOCKED_TECHNICAL; if (!evidence.reviewedAdr()) return BLOCKED_MISSING_ADR; return ELIGIBLE_FOR_STABLE_REVIEW; } } ``` Implement every file and invariant listed under **Implementation requirements**; the snippet fixes the public names and central behavior rather than replacing those requirements. - [ ] **Step 4: Run the focused test and the module test suite** Run: ```bash ./gradlew :modules:jpa-experimental:jpa-next-compatibility:test --tests 'io.backend.skeleton.jpa.experimental.next.ExperimentalPromotionGateTest' ./gradlew :modules:jpa-experimental:jpa-next-compatibility:test ``` Expected: PASS with all assertions green. - [ ] **Step 5: Commit the independently reviewable change** ```bash git add 'modules/jpa-experimental/jpa-next-compatibility/src/compatibilityPostgresql19/java/io/backend/skeleton/jpa/experimental/next/PostgreSql19CompatibilityTest.java' 'docs/jpa/experimental-support-matrix.md' 'docs/jpa/experimental-promotion-checklist.md' '.github/workflows/jpa-next-postgresql19.yml' 'modules/jpa-experimental/jpa-next-compatibility/build.gradle.kts' 'modules/jpa-experimental/jpa-next-compatibility/src/test/java/io/backend/skeleton/jpa/experimental/next/ExperimentalPromotionGateTest.java' git commit -m "docs: add jpa experimental promotion gates" ``` ## 2. Experimental 완료 조건 ```text Stable starter가 Experimental module에 의존하지 않는다. Tenant context 누락과 connection reuse에서 fail-closed다. RLS runtime role이 policy를 bypass하지 못한다. Schema/database tenant migration과 pool capacity가 bounded다. Replica routing이 read-after-write와 lock query를 primary에 고정한다. JPA4/Hibernate8/PG19 lane이 Stable artifacts를 변경하지 않는다. 승격은 ADR와 compatibility/security/failure/migration/performance 증거를 요구한다. ```