36 KiB
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 파일 구조
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-apiand 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
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:
./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
package io.backend.skeleton.jpa.experimental;
public final class ExperimentalFeatureGate {
public void requireEnabled(
ExperimentalFeature feature,
Map<String, Boolean> 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:
./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
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
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:
./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
package io.backend.skeleton.jpa.experimental.tenant;
public final class TenantContext {
private static final ThreadLocal<TenantId> 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:
./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
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_configbefore 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
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:
./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
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:
./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
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
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:
./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
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:
./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
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
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:
./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
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:
./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
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=truealone. -
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
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:
./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
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:
./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
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
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:
./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
testing {
suites {
register<JvmTestSuite>("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:
./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
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
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:
./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
testing {
suites {
register<JvmTestSuite>("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:
./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
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
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:
./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
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:
./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
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 완료 조건
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 증거를 요구한다.