Files
clean-architecture-backend-…/jpa-superpowers-package/docs/superpowers/plans/2026-08-11-jpa-persistence-experimental-expansion-plan.md
T

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-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

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_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

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=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

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 증거를 요구한다.