201 KiB
JPA 관계형 영속성 플랫폼 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: Java/Spring Backend Skeleton에 도메인 Repository 소유권, Application Use Case Transaction, SQLSTATE 기반 오류, 전체 Transaction Retry, Fetch·Pagination·Batch 검증, PostgreSQL Native Extension, Flyway Schema Gate, 관측성·보안·실제 PostgreSQL Release Matrix를 갖춘 JPA 관계형 영속성 플랫폼을 구현한다.
Architecture: jpa-core-api는 Spring·JPA 비종속 안정 계약을 소유하고, jpa-transaction, jpa-spring-data, jpa-hibernate, jpa-postgresql, jpa-migration-flyway가 이를 구현한다. 도메인 모듈은 Entity와 Repository를 직접 소유하며 플랫폼은 Generic CRUD Repository를 만들지 않는다. Retry는 새 Persistence Context의 전체 Use Case 단위이고 Commit 결과 불명은 자동 Retry하지 않는다.
Tech Stack: Java 21, Gradle Kotlin DSL, Spring Boot 4.1 dependency management, Spring Data JPA 4.1, Jakarta Persistence 3.2, Hibernate ORM 7.4, PostgreSQL 16·17·18, HikariCP, Flyway, Micrometer, Spring Observation, JUnit 5, AssertJ, ArchUnit, Testcontainers, Toxiproxy.
Global Constraints
- Root package는
io.backend.skeleton.jpa이다. - 모듈 루트는
modules/jpa이다. - Java 21과 Spring Boot 4.1 BOM 조합을 사용하며 개별 Hibernate·Flyway·Hikari 버전을 임의로 override하지 않는다.
- Stable JPA 규격은 Jakarta Persistence 3.2, Stable Provider는 Hibernate ORM 7.4다.
- Stable DB Matrix는 PostgreSQL 16·17·18이다.
- H2는 Local Convenience이며 PostgreSQL 계약 증거로 사용하지 않는다.
- 도메인 모듈이 Entity, Embeddable, Repository, Query, Index Requirement, Lock·Soft Delete·Audit 정책을 소유한다.
GenericRepository<T, ID>또는 Spring Data CRUD를 재구현하는 Base Repository를 만들지 않는다.- 일반 애플리케이션의 Transaction 경계는 Application Service다.
- OSIV는 모든 운영 profile에서 명시적으로 false다.
- 운영 Schema 변경의 Source of Truth는 Flyway이고 Hibernate는 validate만 수행한다.
- 운영에서
ddl-auto=update,create,create-drop을 허용하지 않는다. - Optimistic Conflict·Deadlock·Serialization Failure Retry는 새 Persistence Context와 새 DB Transaction에서 전체 Use Case를 재실행한다.
TransactionCompletionUnknownException은 자동 Retry하지 않는다.- 외부 HTTP, Object Storage, Messaging 호출을 DB Transaction 안에서 대기하지 않는다.
- PostgreSQL write-heavy Entity의 기본 ID 전략은 Sequence이며 IDENTITY는 제한한다.
- Entity를 Controller 응답, Message payload, Redis Java serialization 값으로 직접 노출하지 않는다.
- Fetch 전략은 Use Case별 EntityGraph·Fetch Join·Projection·Batch Fetch로 결정한다.
- Hibernate 7.4 collection fetch pagination은 PG16·17·18 generated SQL과 row amplification을 계약 테스트한다.
- Dynamic Sort는 allowlist를 사용하고 Native SQL 값은 parameter binding한다.
- JDBC Batch 완료는 실제 batch 통계로 증명한다.
- Bulk DML은 flush → bulk → clear 규칙을 따른다.
- Runtime·Migration·Admin DB credential을 분리한다.
- SQL parameter, Entity ID, Tenant ID 원문, PII를 metric label과 일반 로그에 기록하지 않는다.
- Multi-tenancy, Read Replica, JPA 4, Hibernate 8, PostgreSQL 19는 별도 Experimental 계획으로 구현한다.
- 각 Task는 실패 테스트 → 실패 확인 → 최소 구현 → 통과 확인 → 커밋 순서로 수행한다.
- 각 Task는 독립적으로 검토 가능한 하나의 커밋으로 종료한다.
1. 확정 파일 구조
backend-skeleton/
├── settings.gradle.kts
├── build-logic/src/main/kotlin/jpa-library-conventions.gradle.kts
├── modules/jpa/
│ ├── jpa-core-api/
│ ├── jpa-transaction/
│ ├── jpa-spring-data/
│ ├── jpa-querydsl/
│ ├── jpa-hibernate/
│ ├── jpa-postgresql/
│ ├── jpa-postgresql-copy/
│ ├── jpa-migration-flyway/
│ ├── jpa-auditing/
│ ├── jpa-envers/
│ ├── jpa-cache-hibernate/
│ ├── jpa-observability/
│ ├── jpa-security/
│ ├── jpa-spring-boot-starter/
│ ├── jpa-testkit/
│ ├── jpa-testkit-postgresql/
│ ├── jpa-testkit-migration/
│ └── jpa-testkit-queryplan/
├── infra/jpa/
│ ├── postgres/
│ ├── roles/
│ └── toxiproxy/
├── docs/jpa/
│ ├── support-matrix.md
│ ├── entity-mapping-guide.md
│ ├── transaction-guide.md
│ ├── query-fetch-guide.md
│ ├── migration-guide.md
│ ├── postgresql-extensions.md
│ ├── observability.md
│ ├── security.md
│ └── runbooks.md
└── docs/superpowers/specs/2026-08-11-jpa-persistence-platform-design.md
2. 핵심 package
io.backend.skeleton.jpa.api
io.backend.skeleton.jpa.api.capability
io.backend.skeleton.jpa.api.error
io.backend.skeleton.jpa.api.query
io.backend.skeleton.jpa.api.transaction
io.backend.skeleton.jpa.transaction
io.backend.skeleton.jpa.springdata
io.backend.skeleton.jpa.querydsl
io.backend.skeleton.jpa.hibernate
io.backend.skeleton.jpa.postgresql
io.backend.skeleton.jpa.migration
io.backend.skeleton.jpa.auditing
io.backend.skeleton.jpa.envers
io.backend.skeleton.jpa.cache
io.backend.skeleton.jpa.observation
io.backend.skeleton.jpa.security
io.backend.skeleton.jpa.autoconfigure
io.backend.skeleton.jpa.testkit
3. Module dependency map
jpa-core-api
→ no project dependency
jpa-transaction
→ jpa-core-api
jpa-spring-data
→ jpa-core-api
jpa-querydsl
→ jpa-core-api
→ jpa-spring-data
jpa-hibernate
→ jpa-core-api
jpa-postgresql
→ jpa-core-api
→ jpa-hibernate
jpa-postgresql-copy
→ jpa-core-api
→ jpa-postgresql
jpa-migration-flyway
→ jpa-core-api
jpa-auditing
→ jpa-core-api
jpa-envers
→ jpa-core-api
→ jpa-hibernate
jpa-cache-hibernate
→ jpa-core-api
→ jpa-hibernate
jpa-observability
→ jpa-core-api
→ jpa-hibernate
jpa-security
→ jpa-core-api
jpa-spring-boot-starter
→ jpa-core-api
→ jpa-transaction
→ jpa-spring-data
→ jpa-hibernate
→ jpa-postgresql
→ jpa-migration-flyway
→ jpa-auditing
→ jpa-observability
→ jpa-security
jpa-testkit
→ jpa-core-api
jpa-testkit-postgresql
→ jpa-testkit
→ jpa-postgresql
jpa-testkit-migration
→ jpa-testkit-postgresql
→ jpa-migration-flyway
jpa-testkit-queryplan
→ jpa-testkit-postgresql
→ jpa-observability
Provider SDK, Spring Data, Hibernate, Flyway, Querydsl, PostgreSQL JDBC dependencies are added only in the owning module. jpa-core-api remains framework-free.
Task 1: Gradle 멀티모듈과 JPA 품질 Test Suite 구성
Files:
- Create:
build-logic/src/main/kotlin/jpa-library-conventions.gradle.kts - Create:
modules/jpa/jpa-core-api/build.gradle.kts - Create:
modules/jpa/jpa-transaction/build.gradle.kts - Create:
modules/jpa/jpa-spring-data/build.gradle.kts - Create:
modules/jpa/jpa-querydsl/build.gradle.kts - Create:
modules/jpa/jpa-hibernate/build.gradle.kts - Create:
modules/jpa/jpa-postgresql/build.gradle.kts - Create:
modules/jpa/jpa-postgresql-copy/build.gradle.kts - Create:
modules/jpa/jpa-migration-flyway/build.gradle.kts - Create:
modules/jpa/jpa-auditing/build.gradle.kts - Create:
modules/jpa/jpa-envers/build.gradle.kts - Create:
modules/jpa/jpa-cache-hibernate/build.gradle.kts - Create:
modules/jpa/jpa-observability/build.gradle.kts - Create:
modules/jpa/jpa-security/build.gradle.kts - Create:
modules/jpa/jpa-spring-boot-starter/build.gradle.kts - Create:
modules/jpa/jpa-testkit/build.gradle.kts - Create:
modules/jpa/jpa-testkit-postgresql/build.gradle.kts - Create:
modules/jpa/jpa-testkit-migration/build.gradle.kts - Create:
modules/jpa/jpa-testkit-queryplan/build.gradle.kts - Modify:
settings.gradle.kts - Test:
build-logic/src/test/kotlin/JpaModuleBoundaryTest.kt
Interfaces:
- Consumes: Host repository version catalog and Spring Boot 4.1 dependency management.
- Produces: 18 isolated JPA modules and
test,integrationTest,contractTest,migrationTest,failureTest,performanceTest,compatibilityTestsuites.
Implementation requirements:
-
Register every module under
:modules:jpa:*and apply Java 21 toolchains. -
Do not pin Hibernate, Flyway, Hikari, Spring Data versions outside the Boot BOM.
-
Expose integration suites only in modules that own external resources.
-
Make
checkdepend on unit and architecture tests; release aggregates are added in Task 53. -
Ensure experimental modules are not included in this Stable dependency graph.
-
Step 1: Write the failing test
class JpaModuleBoundaryTest {
@Test
fun `core api has no framework dependency`() {
val core = project(":modules:jpa:jpa-core-api")
assertThat(core.directDependencies())
.noneMatch { it.startsWith("org.springframework") ||
it.startsWith("org.hibernate") ||
it.startsWith("jakarta.persistence") }
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-core-api:test --tests 'JpaModuleBoundaryTest'
Expected: FAIL because the production type or behavior does not exist yet.
- Step 3: Implement the smallest complete production contract
plugins {
`java-library`
`jvm-test-suite`
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}
testing {
suites {
named<JvmTestSuite>("test") { useJUnitJupiter() }
register<JvmTestSuite>("contractTest") {
useJUnitJupiter()
dependencies { implementation(project()) }
}
}
}
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:jpa-core-api:test --tests 'JpaModuleBoundaryTest'
./gradlew :modules:jpa:jpa-core-api:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'build-logic/src/main/kotlin/jpa-library-conventions.gradle.kts' 'modules/jpa/jpa-core-api/build.gradle.kts' 'modules/jpa/jpa-transaction/build.gradle.kts' 'modules/jpa/jpa-spring-data/build.gradle.kts' 'modules/jpa/jpa-querydsl/build.gradle.kts' 'modules/jpa/jpa-hibernate/build.gradle.kts' 'modules/jpa/jpa-postgresql/build.gradle.kts' 'modules/jpa/jpa-postgresql-copy/build.gradle.kts' 'modules/jpa/jpa-migration-flyway/build.gradle.kts' 'modules/jpa/jpa-auditing/build.gradle.kts' 'modules/jpa/jpa-envers/build.gradle.kts' 'modules/jpa/jpa-cache-hibernate/build.gradle.kts' 'modules/jpa/jpa-observability/build.gradle.kts' 'modules/jpa/jpa-security/build.gradle.kts' 'modules/jpa/jpa-spring-boot-starter/build.gradle.kts' 'modules/jpa/jpa-testkit/build.gradle.kts' 'modules/jpa/jpa-testkit-postgresql/build.gradle.kts' 'modules/jpa/jpa-testkit-migration/build.gradle.kts' 'modules/jpa/jpa-testkit-queryplan/build.gradle.kts' 'settings.gradle.kts' 'build-logic/src/test/kotlin/JpaModuleBoundaryTest.kt'
git commit -m "build: add jpa platform modules and test suites"
Task 2: Core Operation Name과 Capability 계약 구현
Files:
- Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/PersistenceOperationName.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/capability/JpaCapability.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/capability/SupportLevel.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/capability/CapabilitySupport.java - Test:
modules/jpa/jpa-core-api/src/test/java/io/backend/skeleton/jpa/api/PersistenceOperationNameTest.java
Interfaces:
- Consumes: Only Java 21 standard library.
- Produces: Bounded operation names and explicit Stable/Advanced/Experimental capability metadata.
Implementation requirements:
-
Operation names must match
[a-z][a-z0-9.-]{2,95}. -
Capability constraints must be immutable and must not store provider objects.
-
Include capabilities for transaction retry, completion evidence, keyset pagination, batch, PostgreSQL native write, schema gate, L2 cache, Envers.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.api;
class PersistenceOperationNameTest {
@Test
void rejectsDynamicIdentifiers() {
assertThatThrownBy(() -> new PersistenceOperationName("order/" + UUID.randomUUID()))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void acceptsRegisteredLowCardinalityName() {
assertThat(new PersistenceOperationName("order.place").value())
.isEqualTo("order.place");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-core-api:test --tests 'io.backend.skeleton.jpa.api.PersistenceOperationNameTest'
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.api;
public record PersistenceOperationName(String value) {
private static final Pattern FORMAT =
Pattern.compile("[a-z][a-z0-9.-]{2,95}");
public PersistenceOperationName {
if (value == null || !FORMAT.matcher(value).matches()) {
throw new IllegalArgumentException("invalid persistence operation name");
}
}
}
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:jpa-core-api:test --tests 'io.backend.skeleton.jpa.api.PersistenceOperationNameTest'
./gradlew :modules:jpa:jpa-core-api:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/PersistenceOperationName.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/capability/JpaCapability.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/capability/SupportLevel.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/capability/CapabilitySupport.java' 'modules/jpa/jpa-core-api/src/test/java/io/backend/skeleton/jpa/api/PersistenceOperationNameTest.java'
git commit -m "feat: add jpa operation and capability contracts"
Task 3: 안정 JPA 오류 계층과 Failure Context 구현
Files:
- Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/JpaPersistenceException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/JpaFailureContext.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/FailureCategory.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/OptimisticConflictException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/PessimisticLockTimeoutException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/DeadlockDetectedException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/SerializationFailureException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/ConstraintViolationDetails.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/UniqueConstraintViolationException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/ForeignKeyViolationException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/CheckConstraintViolationException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/QueryTimeoutException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/TransactionTimeoutException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/ConnectionUnavailableException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/SchemaMismatchException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/DataCorruptionException.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/TransactionCompletionUnknownException.java - Test:
modules/jpa/jpa-core-api/src/test/java/io/backend/skeleton/jpa/api/error/JpaFailureContextTest.java
Interfaces:
- Consumes:
PersistenceOperationNamefrom Task 2. - Produces: Provider-independent, structured, sanitized persistence exceptions.
Implementation requirements:
-
Every exception preserves operation, SQLSTATE, attempt, retryable, completionUnknown, elapsed and trace ID.
-
Constraint exceptions preserve a registered constraint code and optional bounded database constraint name.
-
Exception messages must never contain SQL parameter values, Entity IDs or PII.
-
TransactionCompletionUnknownExceptionmust always reportcompletionUnknown=trueandretryable=false. -
Step 1: Write the failing test
package io.backend.skeleton.jpa.api.error;
class JpaFailureContextTest {
@Test
void completionUnknownCanNeverBeMarkedRetryable() {
var context = JpaFailureContext.completionUnknown(
new PersistenceOperationName("payment.commit"), "40003", 1, Duration.ofMillis(50), "trace");
assertThat(context.retryable()).isFalse();
assertThat(context.completionUnknown()).isTrue();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-core-api:test --tests 'io.backend.skeleton.jpa.api.error.JpaFailureContextTest'
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.api.error;
public record JpaFailureContext(
PersistenceOperationName operation,
String sqlState,
int transactionAttempt,
boolean retryable,
boolean completionUnknown,
Duration elapsed,
String traceId) {
public static JpaFailureContext completionUnknown(
PersistenceOperationName operation,
String sqlState,
int attempt,
Duration elapsed,
String traceId) {
return new JpaFailureContext(
operation, sqlState, attempt, false, true, elapsed, traceId);
}
}
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:jpa-core-api:test --tests 'io.backend.skeleton.jpa.api.error.JpaFailureContextTest'
./gradlew :modules:jpa:jpa-core-api:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/JpaPersistenceException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/JpaFailureContext.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/FailureCategory.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/OptimisticConflictException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/PessimisticLockTimeoutException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/DeadlockDetectedException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/SerializationFailureException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/ConstraintViolationDetails.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/UniqueConstraintViolationException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/ForeignKeyViolationException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/CheckConstraintViolationException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/QueryTimeoutException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/TransactionTimeoutException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/ConnectionUnavailableException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/SchemaMismatchException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/DataCorruptionException.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/error/TransactionCompletionUnknownException.java' 'modules/jpa/jpa-core-api/src/test/java/io/backend/skeleton/jpa/api/error/JpaFailureContextTest.java'
git commit -m "feat: add stable jpa persistence error model"
Task 4: PostgreSQL SQLSTATE 분류와 예외 변환 구현
Files:
- Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/error/PostgreSqlState.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/error/PostgreSqlFailureClassifier.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/error/PostgreSqlExceptionTranslator.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/error/ConstraintCatalog.java - Test:
modules/jpa/jpa-postgresql/src/test/java/io/backend/skeleton/jpa/postgresql/error/PostgreSqlFailureClassifierTest.java
Interfaces:
- Consumes: Stable exceptions from Task 3 and PostgreSQL
PSQLExceptionstructured fields. - Produces: Message-text-independent SQLSTATE classification for
40001,40003,40P01,23505,23503,23514,55P03.
Implementation requirements:
-
Unwrap Spring, Hibernate, JDBC and PostgreSQL exception chains without parsing localized message text.
-
Map constraint names through a bounded
ConstraintCatalogbefore exposing them. -
Unknown SQLSTATE must remain an explicit UNKNOWN category, not an optimistic guess.
-
Do not classify every connection exception as completion unknown; commit phase evidence is required by Task 6.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.postgresql.error;
class PostgreSqlFailureClassifierTest {
@ParameterizedTest
@CsvSource({
"40001,SERIALIZATION_FAILURE",
"40003,COMPLETION_UNKNOWN",
"40P01,DEADLOCK",
"23505,UNIQUE_CONSTRAINT",
"55P03,LOCK_NOT_AVAILABLE"
})
void classifiesBySqlState(String state, FailureCategory expected) {
assertThat(new PostgreSqlFailureClassifier().classify(state))
.isEqualTo(expected);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-postgresql:test --tests 'io.backend.skeleton.jpa.postgresql.error.PostgreSqlFailureClassifierTest'
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.postgresql.error;
public final class PostgreSqlFailureClassifier {
public FailureCategory classify(String sqlState) {
return switch (sqlState) {
case "40001" -> FailureCategory.SERIALIZATION_FAILURE;
case "40003" -> FailureCategory.COMPLETION_UNKNOWN;
case "40P01" -> FailureCategory.DEADLOCK;
case "23505" -> FailureCategory.UNIQUE_CONSTRAINT;
case "23503" -> FailureCategory.FOREIGN_KEY_CONSTRAINT;
case "23514" -> FailureCategory.CHECK_CONSTRAINT;
case "55P03" -> FailureCategory.LOCK_NOT_AVAILABLE;
default -> FailureCategory.UNKNOWN;
};
}
}
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:jpa-postgresql:test --tests 'io.backend.skeleton.jpa.postgresql.error.PostgreSqlFailureClassifierTest'
./gradlew :modules:jpa:jpa-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/error/PostgreSqlState.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/error/PostgreSqlFailureClassifier.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/error/PostgreSqlExceptionTranslator.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/error/ConstraintCatalog.java' 'modules/jpa/jpa-postgresql/src/test/java/io/backend/skeleton/jpa/postgresql/error/PostgreSqlFailureClassifierTest.java'
git commit -m "feat: translate postgresql sqlstate failures"
Task 5: Transaction Profile과 Retry Profile Core 계약 구현
Files:
- Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/PropagationMode.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/IsolationLevel.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/JitterMode.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/RetryProfile.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/TransactionProfile.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/TransactionAttempt.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/RetryDisposition.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/RetryDecision.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/JpaRetryPolicy.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/JpaTransactionExecutor.java - Test:
modules/jpa/jpa-core-api/src/test/java/io/backend/skeleton/jpa/api/transaction/TransactionProfileTest.java
Interfaces:
- Consumes:
PersistenceOperationName,JpaPersistenceExceptionandFailureCategory. - Produces: Framework-free transaction, attempt and retry contracts.
Implementation requirements:
-
Stable propagation values are REQUIRED, MANDATORY and explicitly opt-in REQUIRES_NEW.
-
Expose DEFAULT, READ_COMMITTED, REPEATABLE_READ and SERIALIZABLE isolation.
-
Require positive finite timeout for write profiles.
-
Require
maxAttempts >= 1; completion unknown is never a retryable failure category. -
RetryDecision must distinguish full transaction retry, reconciliation and fail.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.api.transaction;
class TransactionProfileTest {
@Test
void writeProfileRequiresFiniteTimeout() {
assertThatThrownBy(() -> new TransactionProfile(
"write", PropagationMode.REQUIRED, IsolationLevel.READ_COMMITTED,
Duration.ZERO, false, RetryProfile.none()))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-core-api:test --tests 'io.backend.skeleton.jpa.api.transaction.TransactionProfileTest'
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.api.transaction;
public record TransactionProfile(
String name,
PropagationMode propagation,
IsolationLevel isolation,
Duration timeout,
boolean readOnly,
RetryProfile retryProfile) {
public TransactionProfile {
if (!readOnly && (timeout == null || timeout.isZero() || timeout.isNegative())) {
throw new IllegalArgumentException("write transaction requires positive timeout");
}
}
}
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:jpa-core-api:test --tests 'io.backend.skeleton.jpa.api.transaction.TransactionProfileTest'
./gradlew :modules:jpa:jpa-core-api:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/PropagationMode.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/IsolationLevel.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/JitterMode.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/RetryProfile.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/TransactionProfile.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/TransactionAttempt.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/RetryDisposition.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/RetryDecision.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/JpaRetryPolicy.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/transaction/JpaTransactionExecutor.java' 'modules/jpa/jpa-core-api/src/test/java/io/backend/skeleton/jpa/api/transaction/TransactionProfileTest.java'
git commit -m "feat: define jpa transaction and retry profiles"
Task 6: Commit Evidence를 추적하는 JpaTransactionManager 구현
Files:
- Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/TransactionCompletionEvidence.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/TransactionEvidenceContext.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/EvidenceAwareJpaTransactionManager.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/CommitFailureClassifier.java - Test:
modules/jpa/jpa-transaction/src/test/java/io/backend/skeleton/jpa/transaction/EvidenceAwareJpaTransactionManagerTest.java
Interfaces:
- Consumes: Spring ORM
JpaTransactionManager, Task 3 error model and Task 4 classifier SPI. - Produces: Transaction phase evidence and commit-phase-only completion unknown translation.
Implementation requirements:
-
Track NOT_STARTED, ACTIVE, COMMITTING, COMMITTED, ROLLED_BACK and UNKNOWN per transaction.
-
Set COMMITTING immediately before delegating to the provider commit.
-
Only convert transport/SQLSTATE failures during COMMITTING to completion unknown.
-
Clear ThreadLocal evidence in every success and failure path.
-
Preserve the original provider exception as cause without leaking parameters.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.transaction;
class EvidenceAwareJpaTransactionManagerTest {
@Test
void connectionLossDuringCommitBecomesCompletionUnknown() {
var manager = fixtureThatCommitsThenDropsResponse();
assertThatThrownBy(() -> inTransaction(manager, () -> repository.insert("key-1")))
.isInstanceOf(TransactionCompletionUnknownException.class)
.satisfies(error -> assertThat(((JpaPersistenceException) error)
.context().completionUnknown()).isTrue());
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-transaction:test --tests 'io.backend.skeleton.jpa.transaction.EvidenceAwareJpaTransactionManagerTest'
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.transaction;
public final class EvidenceAwareJpaTransactionManager extends JpaTransactionManager {
private final CommitFailureClassifier classifier;
@Override
protected void doCommit(DefaultTransactionStatus status) {
TransactionEvidenceContext.mark(TransactionCompletionEvidence.COMMITTING);
try {
super.doCommit(status);
TransactionEvidenceContext.mark(TransactionCompletionEvidence.COMMITTED);
} catch (RuntimeException failure) {
TransactionEvidenceContext.mark(TransactionCompletionEvidence.UNKNOWN);
throw classifier.translateCommitFailure(failure);
} finally {
TransactionEvidenceContext.clear();
}
}
}
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:jpa-transaction:test --tests 'io.backend.skeleton.jpa.transaction.EvidenceAwareJpaTransactionManagerTest'
./gradlew :modules:jpa:jpa-transaction:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/TransactionCompletionEvidence.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/TransactionEvidenceContext.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/EvidenceAwareJpaTransactionManager.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/CommitFailureClassifier.java' 'modules/jpa/jpa-transaction/src/test/java/io/backend/skeleton/jpa/transaction/EvidenceAwareJpaTransactionManagerTest.java'
git commit -m "feat: track jpa transaction completion evidence"
Task 7: Programmatic JpaTransactionExecutor 구현
Files:
- Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/SpringJpaTransactionExecutor.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/TransactionDefinitionMapper.java - Test:
modules/jpa/jpa-transaction/src/test/java/io/backend/skeleton/jpa/transaction/SpringJpaTransactionExecutorTest.java
Interfaces:
- Consumes: Task 5 transaction contracts and Spring
PlatformTransactionManager. - Produces: A programmatic transaction boundary that maps profile propagation, isolation, timeout and read-only exactly.
Implementation requirements:
-
Use a fresh
TransactionTemplatedefinition per call without mutable global state. -
Map timeout to whole seconds only after rejecting sub-second truncation or documenting rounding.
-
Propagate
PersistenceOperationNameinto observation context. -
Do not implement retry in this class; Task 8 owns retry coordination.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.transaction;
class SpringJpaTransactionExecutorTest {
@Test
void mapsSerializableReadOnlyProfile() {
var profile = profile(SERIALIZABLE, Duration.ofSeconds(3), true);
executor.execute(OPERATION, profile, () -> null);
assertThat(transactionProbe.isolation()).isEqualTo(Connection.TRANSACTION_SERIALIZABLE);
assertThat(transactionProbe.readOnly()).isTrue();
assertThat(transactionProbe.timeoutSeconds()).isEqualTo(3);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-transaction:test --tests 'io.backend.skeleton.jpa.transaction.SpringJpaTransactionExecutorTest'
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.transaction;
public final class SpringJpaTransactionExecutor implements JpaTransactionExecutor {
private final PlatformTransactionManager transactionManager;
@Override
public <T> T execute(
PersistenceOperationName operation,
TransactionProfile profile,
Supplier<T> work) {
var template = new TransactionTemplate(transactionManager);
TransactionDefinitionMapper.apply(template, profile);
return template.execute(status -> work.get());
}
}
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:jpa-transaction:test --tests 'io.backend.skeleton.jpa.transaction.SpringJpaTransactionExecutorTest'
./gradlew :modules:jpa:jpa-transaction:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/SpringJpaTransactionExecutor.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/TransactionDefinitionMapper.java' 'modules/jpa/jpa-transaction/src/test/java/io/backend/skeleton/jpa/transaction/SpringJpaTransactionExecutorTest.java'
git commit -m "feat: execute jpa transaction profiles"
Task 8: 전체 Transaction Retry Coordinator 구현
Files:
- Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/FullTransactionRetryCoordinator.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/BackoffCalculator.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/RetryBudget.java - Test:
modules/jpa/jpa-transaction/src/test/java/io/backend/skeleton/jpa/transaction/FullTransactionRetryCoordinatorTest.java
Interfaces:
- Consumes:
JpaTransactionExecutor,JpaRetryPolicy,RetryProfileand stable exceptions. - Produces: Bounded retry that calls the transaction executor anew for every attempt.
Implementation requirements:
-
Every attempt must create a new transaction and new Persistence Context.
-
Never retry completion unknown, constraint, schema or data corruption failures.
-
Apply exponential backoff, configured jitter, max elapsed deadline and attempt budget.
-
Emit one logical operation result and attempt-level events without logging every retry as WARN.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.transaction;
class FullTransactionRetryCoordinatorTest {
@Test
void retriesWholeUseCaseWithFreshPersistenceContext() {
var contexts = new ArrayList<Integer>();
var result = coordinator.execute(OPERATION, RETRY_PROFILE, () -> {
contexts.add(entityManagerIdentity());
if (contexts.size() == 1) throw optimisticConflict();
return "ok";
});
assertThat(result).isEqualTo("ok");
assertThat(contexts).hasSize(2).doesNotHaveDuplicates();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-transaction:test --tests 'io.backend.skeleton.jpa.transaction.FullTransactionRetryCoordinatorTest'
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.transaction;
public final class FullTransactionRetryCoordinator {
public <T> T execute(
PersistenceOperationName operation,
TransactionProfile profile,
Supplier<T> work) {
for (int attempt = 1; ; attempt++) {
try {
return transactionExecutor.execute(operation, profile, work);
} catch (JpaPersistenceException failure) {
RetryDecision decision = retryPolicy.classify(
failure, new TransactionAttempt(attempt, clock.instant()));
if (decision.disposition() != RETRY_FULL_TRANSACTION) throw failure;
sleeper.sleep(decision.delay());
}
}
}
}
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:jpa-transaction:test --tests 'io.backend.skeleton.jpa.transaction.FullTransactionRetryCoordinatorTest'
./gradlew :modules:jpa:jpa-transaction:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/FullTransactionRetryCoordinator.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/BackoffCalculator.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/RetryBudget.java' 'modules/jpa/jpa-transaction/src/test/java/io/backend/skeleton/jpa/transaction/FullTransactionRetryCoordinatorTest.java'
git commit -m "feat: retry complete jpa transactions safely"
Task 9: RetryableJpaTransaction Annotation과 AOP ordering 구현
Files:
- Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/RetryableJpaTransaction.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/RetryableJpaTransactionInterceptor.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/TransactionProfileRegistry.java - Test:
modules/jpa/jpa-transaction/src/test/java/io/backend/skeleton/jpa/transaction/RetryableJpaTransactionInterceptorTest.java
Interfaces:
- Consumes: Task 8 coordinator and named transaction profiles.
- Produces: An opt-in public-method annotation whose retry interceptor wraps the Spring transaction interceptor.
Implementation requirements:
-
Require a registered operation name and profile name in the annotation.
-
Order retry advice outside transaction advice so each attempt creates a transaction.
-
Reject self-invocation in documentation and architecture tests.
-
Reject methods that return reactive types because JPA is blocking.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.transaction;
class RetryableJpaTransactionInterceptorTest {
@Test
void retryAdviceRunsOutsideTransactionAdvice() {
service.failOnceWithSerializationFailure();
service.execute();
assertThat(probe.transactionIds()).containsExactly("tx-1", "tx-2");
assertThat(probe.retryAdviceOrder()).isLessThan(probe.transactionAdviceOrder());
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-transaction:test --tests 'io.backend.skeleton.jpa.transaction.RetryableJpaTransactionInterceptorTest'
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.transaction;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryableJpaTransaction {
String operation();
String profile();
}
@Order(Ordered.HIGHEST_PRECEDENCE + 100)
public final class RetryableJpaTransactionInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) {
var policy = annotation(invocation.getMethod());
return coordinator.execute(
new PersistenceOperationName(policy.operation()),
profiles.require(policy.profile()),
() -> proceed(invocation));
}
}
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:jpa-transaction:test --tests 'io.backend.skeleton.jpa.transaction.RetryableJpaTransactionInterceptorTest'
./gradlew :modules:jpa:jpa-transaction:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/RetryableJpaTransaction.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/RetryableJpaTransactionInterceptor.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/TransactionProfileRegistry.java' 'modules/jpa/jpa-transaction/src/test/java/io/backend/skeleton/jpa/transaction/RetryableJpaTransactionInterceptorTest.java'
git commit -m "feat: add retryable jpa transaction advice"
Task 10: Completion Unknown Reconciliation SPI와 Audit 구현
Files:
- Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/TransactionCompletionResolver.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/CompletionResolution.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/CompletionUnknownRecord.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/CompletionUnknownRecorder.java - Test:
modules/jpa/jpa-transaction/src/test/java/io/backend/skeleton/jpa/transaction/CompletionUnknownRecorderTest.java
Interfaces:
- Consumes:
TransactionCompletionUnknownExceptionand domain-provided transaction keys. - Produces: A durable/auditable handoff for domain-specific committed/not-committed/unknown reconciliation.
Implementation requirements:
-
Core resolver returns COMMITTED, NOT_COMMITTED or STILL_UNKNOWN without guessing.
-
Recording must happen outside the unknown transaction using a separate durable channel chosen by the application.
-
Preserve operation, transaction key, SQLSTATE, trace ID and occurrence time; never persist SQL parameters.
-
Do not automatically call the original use case from the resolver.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.transaction;
class CompletionUnknownRecorderTest {
@Test
void recordsUnknownWithoutRetryingOriginalWork() {
recorder.record(exception("payment-42"));
assertThat(audit.last().transactionKey()).isEqualTo("payment-42");
assertThat(originalUseCase.invocations()).isZero();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-transaction:test --tests 'io.backend.skeleton.jpa.transaction.CompletionUnknownRecorderTest'
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.transaction;
public interface TransactionCompletionResolver<K> {
CompletionResolution resolve(K transactionKey);
}
public enum CompletionResolution {
COMMITTED,
NOT_COMMITTED,
STILL_UNKNOWN
}
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:jpa-transaction:test --tests 'io.backend.skeleton.jpa.transaction.CompletionUnknownRecorderTest'
./gradlew :modules:jpa:jpa-transaction:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/TransactionCompletionResolver.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/CompletionResolution.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/CompletionUnknownRecord.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/CompletionUnknownRecorder.java' 'modules/jpa/jpa-transaction/src/test/java/io/backend/skeleton/jpa/transaction/CompletionUnknownRecorderTest.java'
git commit -m "feat: add transaction completion reconciliation contracts"
Task 11: OSIV와 DDL Auto 위험 설정 Startup Guard 구현
Files:
- Create:
modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaSafetyProperties.java - Create:
modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaDangerousConfigurationGuard.java - Modify:
modules/jpa/jpa-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - Test:
modules/jpa/jpa-spring-boot-starter/src/test/java/io/backend/skeleton/jpa/autoconfigure/JpaDangerousConfigurationGuardTest.java
Interfaces:
- Consumes: Spring Boot Environment and the design global constraints.
- Produces: Fail-fast startup validation for OSIV and production schema mutation settings.
Implementation requirements:
-
Fail when
spring.jpa.open-in-view=trueoutside an explicit local convenience profile. -
Fail in dev/staging/prod when ddl-auto is update/create/create-drop.
-
Allow validate or none according to schema-management policy.
-
Error messages must name the unsafe property and approved alternatives.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.autoconfigure;
class JpaDangerousConfigurationGuardTest {
@Test
void productionRejectsOpenSessionInViewAndDdlUpdate() {
context.withPropertyValues(
"spring.profiles.active=prod",
"spring.jpa.open-in-view=true",
"spring.jpa.hibernate.ddl-auto=update")
.run(result -> assertThat(result).hasFailed());
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-spring-boot-starter:test --tests 'io.backend.skeleton.jpa.autoconfigure.JpaDangerousConfigurationGuardTest'
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.autoconfigure;
public final class JpaDangerousConfigurationGuard {
public void validate(Environment environment) {
boolean osiv = environment.getProperty(
"spring.jpa.open-in-view", Boolean.class, false);
String ddl = environment.getProperty(
"spring.jpa.hibernate.ddl-auto", "none");
if (osiv) throw new IllegalStateException("spring.jpa.open-in-view must be false");
if (Set.of("update", "create", "create-drop").contains(ddl)) {
throw new IllegalStateException("Flyway owns schema changes; use validate or none");
}
}
}
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:jpa-spring-boot-starter:test --tests 'io.backend.skeleton.jpa.autoconfigure.JpaDangerousConfigurationGuardTest'
./gradlew :modules:jpa:jpa-spring-boot-starter:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaSafetyProperties.java' 'modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaDangerousConfigurationGuard.java' 'modules/jpa/jpa-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports' 'modules/jpa/jpa-spring-boot-starter/src/test/java/io/backend/skeleton/jpa/autoconfigure/JpaDangerousConfigurationGuardTest.java'
git commit -m "feat: reject unsafe jpa startup configuration"
Task 12: Hikari·PostgreSQL Runtime Profile 검증 구현
Files:
- Create:
modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaDataSourceProperties.java - Create:
modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaDataSourceProfileValidator.java - Create:
modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/PostgreSqlVersionPolicy.java - Test:
modules/jpa/jpa-spring-boot-starter/src/test/java/io/backend/skeleton/jpa/autoconfigure/JpaDataSourceProfileValidatorTest.java
Interfaces:
- Consumes: Configured DataSource metadata, Hikari configuration and Stable PG16·17·18 policy.
- Produces: Runtime validation for database product/version, explicit pool limits and finite acquisition timeout.
Implementation requirements:
-
Reject non-PostgreSQL production datasource unless a future profile is explicitly installed.
-
Accept PostgreSQL 16, 17 and 18; report but do not Stable-enable 19.
-
Require explicit maximumPoolSize and connectionTimeout in production properties.
-
Do not impose a universal pool size; validate consistency with positive bounds only.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.autoconfigure;
class JpaDataSourceProfileValidatorTest {
@Test
void rejectsPostgreSqlNineteenFromStableProfile() {
var metadata = metadata("PostgreSQL", 19);
assertThatThrownBy(() -> validator.validateStable(metadata, properties()))
.hasMessageContaining("PostgreSQL 16, 17 or 18");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-spring-boot-starter:test --tests 'io.backend.skeleton.jpa.autoconfigure.JpaDataSourceProfileValidatorTest'
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.autoconfigure;
public final class PostgreSqlVersionPolicy {
private static final Set<Integer> STABLE = Set.of(16, 17, 18);
public void requireStable(DatabaseMetaData metadata) throws SQLException {
if (!"PostgreSQL".equals(metadata.getDatabaseProductName()) ||
!STABLE.contains(metadata.getDatabaseMajorVersion())) {
throw new IllegalStateException("Stable JPA profile requires PostgreSQL 16, 17 or 18");
}
}
}
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:jpa-spring-boot-starter:test --tests 'io.backend.skeleton.jpa.autoconfigure.JpaDataSourceProfileValidatorTest'
./gradlew :modules:jpa:jpa-spring-boot-starter:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaDataSourceProperties.java' 'modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaDataSourceProfileValidator.java' 'modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/PostgreSqlVersionPolicy.java' 'modules/jpa/jpa-spring-boot-starter/src/test/java/io/backend/skeleton/jpa/autoconfigure/JpaDataSourceProfileValidatorTest.java'
git commit -m "feat: validate jpa datasource and postgresql profile"
Task 13: Entity Mapping ArchUnit Rule Pack 구현
Files:
- Create:
modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/JpaArchitectureRules.java - Create:
modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/EntityMappingCondition.java - Create:
modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/EntityExposureCondition.java - Test:
modules/jpa/jpa-security/src/test/java/io/backend/skeleton/jpa/security/JpaArchitectureRulesTest.java
Interfaces:
- Consumes: ArchUnit and Jakarta Persistence annotations in the inspected application.
- Produces: Reusable rules for field access, non-final Entity, protected no-arg constructor, no web exposure and no Hibernate dependency in domain packages.
Implementation requirements:
-
Detect Controller methods returning an
@Entitytype or collection of Entity. -
Detect Entity classes in web/controller packages.
-
Detect
org.hibernatedependencies from domain packages. -
Detect final Entity classes and missing protected/public no-arg constructors.
-
Provide separate warning-level rules for Cascade.ALL and EAGER associations rather than silently rewriting them.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.security;
class JpaArchitectureRulesTest {
@Test
void controllerMayNotReturnEntity() {
var classes = new ClassFileImporter().importClasses(BadOrderController.class, OrderEntity.class);
assertThatThrownBy(() -> JpaArchitectureRules.noEntityFromWeb().check(classes))
.hasMessageContaining("OrderEntity");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-security:test --tests 'io.backend.skeleton.jpa.security.JpaArchitectureRulesTest'
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.security;
public final class JpaArchitectureRules {
public static ArchRule noEntityFromWeb() {
return methods().that().areDeclaredInClassesThat()
.resideInAPackage("..web..")
.should(new EntityExposureCondition());
}
public static ArchRule entitiesFollowPortableMappingRules() {
return classes().that().areAnnotatedWith(Entity.class)
.should(new EntityMappingCondition());
}
}
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:jpa-security:test --tests 'io.backend.skeleton.jpa.security.JpaArchitectureRulesTest'
./gradlew :modules:jpa:jpa-security:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/JpaArchitectureRules.java' 'modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/EntityMappingCondition.java' 'modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/EntityExposureCondition.java' 'modules/jpa/jpa-security/src/test/java/io/backend/skeleton/jpa/security/JpaArchitectureRulesTest.java'
git commit -m "feat: enforce jpa entity architecture rules"
Task 14: Sequence·UUID ID Strategy Contract Testkit 구현
Files:
- Create:
modules/jpa/jpa-testkit/src/main/java/io/backend/skeleton/jpa/testkit/id/UuidV7Generator.java - Create:
modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/id/SequenceEntity.java - Create:
modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/id/IdentityEntity.java - Create:
modules/jpa/jpa-testkit-postgresql/src/contractTest/java/io/backend/skeleton/jpa/testkit/id/IdStrategyContractTest.java - Test:
modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/id/UuidV7GeneratorTest.java
Interfaces:
- Consumes: PostgreSQL Testcontainer foundation and Hibernate statistics.
- Produces: Application UUIDv7 generator and evidence that Sequence batches while IDENTITY is classified as limited.
Implementation requirements:
-
UUIDv7 output must be monotonic enough for the test clock and set RFC variant/version bits.
-
Sequence fixture must align allocationSize with the migration sequence increment.
-
Contract test records actual prepared statements and JDBC batches.
-
Do not expose PostgreSQL 18
uuidv7()as PG16·17 common behavior. -
Step 1: Write the failing test
package io.backend.skeleton.jpa.testkit.id;
class UuidV7GeneratorTest {
@Test
void producesVersionSevenUuidInTimeOrder() {
var first = generator.next(Instant.parse("2026-08-11T00:00:00Z"));
var second = generator.next(Instant.parse("2026-08-11T00:00:01Z"));
assertThat(first.version()).isEqualTo(7);
assertThat(first.compareTo(second)).isLessThan(0);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.id.UuidV7GeneratorTest'
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.testkit.id;
public final class UuidV7Generator {
public UUID next(Instant instant) {
long unixMillis = instant.toEpochMilli() & 0x0000_FFFF_FFFF_FFFFL;
long most = (unixMillis << 16) | 0x7000L | random.nextLong(0x1000L);
long least = (random.nextLong() & 0x3FFF_FFFF_FFFF_FFFFL) |
0x8000_0000_0000_0000L;
return new UUID(most, least);
}
}
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:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.id.UuidV7GeneratorTest'
./gradlew :modules:jpa:jpa-testkit-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-testkit/src/main/java/io/backend/skeleton/jpa/testkit/id/UuidV7Generator.java' 'modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/id/SequenceEntity.java' 'modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/id/IdentityEntity.java' 'modules/jpa/jpa-testkit-postgresql/src/contractTest/java/io/backend/skeleton/jpa/testkit/id/IdStrategyContractTest.java' 'modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/id/UuidV7GeneratorTest.java'
git commit -m "test: add jpa id strategy contracts"
Task 15: JPA 3.2 Value Mapping Contract Fixture 구현
Files:
- Create:
modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/mapping/Money.java - Create:
modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/mapping/MappingEntity.java - Create:
modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/mapping/DurationMillisConverter.java - Create:
modules/jpa/jpa-testkit-postgresql/src/contractTest/java/io/backend/skeleton/jpa/testkit/mapping/JpaValueMappingContractTest.java - Test:
modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/mapping/DurationMillisConverterTest.java
Interfaces:
- Consumes: JPA 3.2, Hibernate 7.4 and PostgreSQL round-trip test infrastructure.
- Produces: Round-trip contracts for Instant, OffsetDateTime, LocalDate, UUID, String Enum, record Embeddable and Duration converter.
Implementation requirements:
-
Use STRING or explicit converter for Enum; never ORDINAL.
-
Verify record Embeddable construction and dirty checking under Hibernate 7.4.
-
Specify timezone and precision assertions explicitly.
-
Malformed database values must produce stable data corruption errors.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.testkit.mapping;
class DurationMillisConverterTest {
@Test
void roundTripsDurationAsMilliseconds() {
var duration = Duration.ofSeconds(42).plusMillis(7);
assertThat(converter.convertToEntityAttribute(
converter.convertToDatabaseColumn(duration))).isEqualTo(duration);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.mapping.DurationMillisConverterTest'
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.testkit.mapping;
@Converter(autoApply = false)
public final class DurationMillisConverter
implements AttributeConverter<Duration, Long> {
public Long convertToDatabaseColumn(Duration value) {
return value == null ? null : value.toMillis();
}
public Duration convertToEntityAttribute(Long value) {
return value == null ? null : Duration.ofMillis(value);
}
}
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:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.mapping.DurationMillisConverterTest'
./gradlew :modules:jpa:jpa-testkit-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/mapping/Money.java' 'modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/mapping/MappingEntity.java' 'modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/mapping/DurationMillisConverter.java' 'modules/jpa/jpa-testkit-postgresql/src/contractTest/java/io/backend/skeleton/jpa/testkit/mapping/JpaValueMappingContractTest.java' 'modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/mapping/DurationMillisConverterTest.java'
git commit -m "test: define jpa value mapping contracts"
Task 16: Entity Lifecycle·Association Persistence Context Contract 구현
Files:
- Create:
modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/lifecycle/LifecycleParent.java - Create:
modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/lifecycle/LifecycleChild.java - Create:
modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/lifecycle/EntityStateProbe.java - Create:
modules/jpa/jpa-testkit-postgresql/src/contractTest/java/io/backend/skeleton/jpa/testkit/lifecycle/JpaLifecycleAssociationContractTest.java - Test:
modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/lifecycle/EntityStateProbeTest.java
Interfaces:
- Consumes: Jakarta Persistence 3.2 EntityManager lifecycle and domain-style parent/child fixtures.
- Produces: Explicit persist, merge, find, getReference, dirty-check, flush, clear, detach, refresh, owning-side, cascade and orphan-removal contracts.
Implementation requirements:
-
Prove
mergereturns the managed copy and does not attach the passed detached instance. -
Prove flush writes SQL but does not imply transaction commit.
-
Prove clear/detach stop dirty checking and refresh reloads database state.
-
Prove only the owning side updates the foreign key and helper methods synchronize both sides.
-
Test cascade/orphan removal only on an aggregate-owned child fixture; do not define a platform-wide default.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.testkit.lifecycle;
class EntityStateProbeTest {
@Test
void distinguishesManagedDetachedAndMergedInstances() {
var original = new LifecycleParent("p-1");
entityManager.persist(original);
entityManager.flush();
entityManager.detach(original);
var merged = entityManager.merge(original);
assertThat(entityManager.contains(original)).isFalse();
assertThat(entityManager.contains(merged)).isTrue();
assertThat(merged).isNotSameAs(original);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.lifecycle.EntityStateProbeTest'
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.testkit.lifecycle;
public final class EntityStateProbe {
private final EntityManager entityManager;
public EntityState stateOf(Object entity) {
if (entityManager.contains(entity)) return EntityState.MANAGED;
Object id = entityManager.getEntityManagerFactory()
.getPersistenceUnitUtil().getIdentifier(entity);
return id == null ? EntityState.TRANSIENT : EntityState.DETACHED;
}
}
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:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.lifecycle.EntityStateProbeTest'
./gradlew :modules:jpa:jpa-testkit-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/lifecycle/LifecycleParent.java' 'modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/lifecycle/LifecycleChild.java' 'modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/lifecycle/EntityStateProbe.java' 'modules/jpa/jpa-testkit-postgresql/src/contractTest/java/io/backend/skeleton/jpa/testkit/lifecycle/JpaLifecycleAssociationContractTest.java' 'modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/lifecycle/EntityStateProbeTest.java'
git commit -m "test: add jpa lifecycle and association contracts"
Task 17: Spring Data Auditing Opt-in 모듈 구현
Files:
- Create:
modules/jpa/jpa-auditing/src/main/java/io/backend/skeleton/jpa/auditing/AuditMetadata.java - Create:
modules/jpa/jpa-auditing/src/main/java/io/backend/skeleton/jpa/auditing/JpaAuditorProvider.java - Create:
modules/jpa/jpa-auditing/src/main/java/io/backend/skeleton/jpa/auditing/JpaAuditingConfiguration.java - Test:
modules/jpa/jpa-auditing/src/test/java/io/backend/skeleton/jpa/auditing/JpaAuditingContractTest.java
Interfaces:
- Consumes: Spring Data auditing and application-provided current actor resolver.
- Produces: Embeddable technical auditing without a mandatory BaseEntity.
Implementation requirements:
-
Provide createdAt, createdBy, modifiedAt and modifiedBy as an opt-in Embeddable.
-
Use
Instantand a bounded opaque actor identifier. -
Do not confuse technical auditing with business audit or Entity history.
-
Allow system/background jobs to use an explicit system actor.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.auditing;
class JpaAuditingContractTest {
@Test
void persistsTechnicalAuditWhenEntityOptsIn() {
var saved = repository.save(new AuditedFixture("value"));
entityManager.flush();
assertThat(saved.audit().createdAt()).isEqualTo(clock.instant());
assertThat(saved.audit().createdBy()).isEqualTo("user-42");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-auditing:test --tests 'io.backend.skeleton.jpa.auditing.JpaAuditingContractTest'
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.auditing;
@Embeddable
public class AuditMetadata {
@CreatedDate private Instant createdAt;
@CreatedBy private String createdBy;
@LastModifiedDate private Instant modifiedAt;
@LastModifiedBy private String modifiedBy;
protected AuditMetadata() {}
}
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:jpa-auditing:test --tests 'io.backend.skeleton.jpa.auditing.JpaAuditingContractTest'
./gradlew :modules:jpa:jpa-auditing:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-auditing/src/main/java/io/backend/skeleton/jpa/auditing/AuditMetadata.java' 'modules/jpa/jpa-auditing/src/main/java/io/backend/skeleton/jpa/auditing/JpaAuditorProvider.java' 'modules/jpa/jpa-auditing/src/main/java/io/backend/skeleton/jpa/auditing/JpaAuditingConfiguration.java' 'modules/jpa/jpa-auditing/src/test/java/io/backend/skeleton/jpa/auditing/JpaAuditingContractTest.java'
git commit -m "feat: add opt in spring data jpa auditing"
Task 18: QueryName과 QueryObservation Core 구현
Files:
- Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/QueryName.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/QueryObservation.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/QueryScope.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/NoopQueryObservation.java - Test:
modules/jpa/jpa-core-api/src/test/java/io/backend/skeleton/jpa/api/query/QueryNameTest.java
Interfaces:
- Consumes: Java 21 only and the operation-name validation pattern.
- Produces: Low-cardinality query identity and framework-neutral observation scopes.
Implementation requirements:
-
Query names use a bounded registry format and never contain IDs or raw SQL.
-
QueryScope records row count, failure and close exactly once.
-
Provide a no-op implementation for modules that do not install observability.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.api.query;
class QueryNameTest {
@Test
void rejectsRawSqlAsMetricIdentity() {
assertThatThrownBy(() -> new QueryName("select * from orders where id=42"))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-core-api:test --tests 'io.backend.skeleton.jpa.api.query.QueryNameTest'
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.api.query;
public record QueryName(String value) {
public QueryName {
if (value == null || !value.matches("[a-z][a-z0-9.-]{2,95}")) {
throw new IllegalArgumentException("invalid query name");
}
}
}
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:jpa-core-api:test --tests 'io.backend.skeleton.jpa.api.query.QueryNameTest'
./gradlew :modules:jpa:jpa-core-api:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/QueryName.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/QueryObservation.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/QueryScope.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/NoopQueryObservation.java' 'modules/jpa/jpa-core-api/src/test/java/io/backend/skeleton/jpa/api/query/QueryNameTest.java'
git commit -m "feat: add bounded jpa query observation contract"
Task 19: Custom Repository Fragment 지원과 Generic Repository 금지 규칙 구현
Files:
- Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/JpaRepositoryFragmentSupport.java - Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/EntityManagerAccess.java - Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/RegisteredQuery.java - Modify:
modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/JpaArchitectureRules.java - Test:
modules/jpa/jpa-spring-data/src/test/java/io/backend/skeleton/jpa/springdata/JpaRepositoryFragmentSupportTest.java
Interfaces:
- Consumes: Spring Data JPA custom fragment model and Task 18 query names.
- Produces: A helper base for domain-owned custom implementations, not a CRUD repository.
Implementation requirements:
-
Do not declare save, findById, findAll or delete methods in platform interfaces.
-
Expose EntityManager only to custom repository implementation packages.
-
Require a registered QueryName for helper-created typed/native queries.
-
Add an architecture test that fails if a platform type named GenericRepository or BaseRepository extends CrudRepository.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.springdata;
class JpaRepositoryFragmentSupportTest {
@Test
void platformDoesNotReimplementCrudRepository() {
assertThat(JpaRepositoryFragmentSupport.class.getMethods())
.extracting(Method::getName)
.doesNotContain("save", "findById", "findAll", "delete");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-spring-data:test --tests 'io.backend.skeleton.jpa.springdata.JpaRepositoryFragmentSupportTest'
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.springdata;
public abstract class JpaRepositoryFragmentSupport {
private final EntityManager entityManager;
protected JpaRepositoryFragmentSupport(EntityManager entityManager) {
this.entityManager = entityManager;
}
protected final <T> TypedQuery<T> typedQuery(
QueryName name, String jpql, Class<T> resultType) {
return entityManager.createQuery(jpql, resultType)
.setHint("org.hibernate.comment", name.value());
}
}
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:jpa-spring-data:test --tests 'io.backend.skeleton.jpa.springdata.JpaRepositoryFragmentSupportTest'
./gradlew :modules:jpa:jpa-spring-data:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/JpaRepositoryFragmentSupport.java' 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/EntityManagerAccess.java' 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/RegisteredQuery.java' 'modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/JpaArchitectureRules.java' 'modules/jpa/jpa-spring-data/src/test/java/io/backend/skeleton/jpa/springdata/JpaRepositoryFragmentSupportTest.java'
git commit -m "feat: support domain owned jpa repository fragments"
Task 20: Specification과 Querydsl 선택 Integration 구현
Files:
- Create:
modules/jpa/jpa-querydsl/src/main/java/io/backend/skeleton/jpa/querydsl/QuerydslJpaSupport.java - Create:
modules/jpa/jpa-querydsl/src/main/java/io/backend/skeleton/jpa/querydsl/PredicatePolicy.java - Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/SpecificationPolicy.java - Test:
modules/jpa/jpa-querydsl/src/test/java/io/backend/skeleton/jpa/querydsl/QuerydslJpaSupportTest.java
Interfaces:
- Consumes: Optional Querydsl JPA dependency, Spring Data Specification and registered QueryName.
- Produces: Explicit Q2 dynamic query helpers without changing J1 repository contracts.
Implementation requirements:
-
Keep Querydsl as an optional module; starter must not pull it transitively unless enabled.
-
Reject an unbounded query when no predicate and no explicit allow-all token is present.
-
Require page size and sort allowlist for collection queries.
-
Do not accept user-provided path expressions.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.querydsl;
class QuerydslJpaSupportTest {
@Test
void rejectsUnboundedPredicateForCollectionQuery() {
assertThatThrownBy(() -> support.select(ORDER_QUERY, order, null, page(100)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("bounded predicate");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-querydsl:test --tests 'io.backend.skeleton.jpa.querydsl.QuerydslJpaSupportTest'
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.querydsl;
public final class QuerydslJpaSupport {
public <T> JPAQuery<T> select(
QueryName name,
EntityPath<T> root,
Predicate predicate,
QueryPage page) {
PredicatePolicy.requireBounded(predicate, page);
return queryFactory.selectFrom(root)
.where(predicate)
.limit(page.size())
.setHint("org.hibernate.comment", name.value());
}
}
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:jpa-querydsl:test --tests 'io.backend.skeleton.jpa.querydsl.QuerydslJpaSupportTest'
./gradlew :modules:jpa:jpa-querydsl:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-querydsl/src/main/java/io/backend/skeleton/jpa/querydsl/QuerydslJpaSupport.java' 'modules/jpa/jpa-querydsl/src/main/java/io/backend/skeleton/jpa/querydsl/PredicatePolicy.java' 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/SpecificationPolicy.java' 'modules/jpa/jpa-querydsl/src/test/java/io/backend/skeleton/jpa/querydsl/QuerydslJpaSupportTest.java'
git commit -m "feat: add optional jpa specification and querydsl support"
Task 21: Dynamic Sort Allowlist와 Safe Sort Mapper 구현
Files:
- Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/SafeSortField.java - Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/SafeSortRegistry.java - Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/SafeSortMapper.java - Test:
modules/jpa/jpa-spring-data/src/test/java/io/backend/skeleton/jpa/springdata/SafeSortMapperTest.java
Interfaces:
- Consumes: Spring Data
Sortand a domain-registered field catalog. - Produces: Injection-safe sort mapping with deterministic tie-breakers.
Implementation requirements:
-
Reject unknown field, function expression, whitespace and punctuation from user input.
-
Map public sort names to fixed entity paths.
-
Append the configured stable tie-breaker when absent.
-
Do not use
JpaSort.unsafefor user-controlled values. -
Step 1: Write the failing test
package io.backend.skeleton.jpa.springdata;
class SafeSortMapperTest {
@Test
void rejectsSqlExpressionAndAddsTieBreaker() {
assertThatThrownBy(() -> mapper.map(List.of("name desc nulls last; drop table")))
.isInstanceOf(IllegalArgumentException.class);
assertThat(mapper.map(List.of("createdAt,desc")))
.extracting(Sort.Order::getProperty)
.containsExactly("createdAt", "id");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-spring-data:test --tests 'io.backend.skeleton.jpa.springdata.SafeSortMapperTest'
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.springdata;
public final class SafeSortMapper {
public Sort map(List<RequestedSort> requested) {
var orders = requested.stream()
.map(value -> registry.require(value.field()).toOrder(value.direction()))
.collect(Collectors.toCollection(ArrayList::new));
if (orders.stream().noneMatch(order -> order.getProperty().equals(registry.tieBreaker()))) {
orders.add(Sort.Order.desc(registry.tieBreaker()));
}
return Sort.by(orders);
}
}
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:jpa-spring-data:test --tests 'io.backend.skeleton.jpa.springdata.SafeSortMapperTest'
./gradlew :modules:jpa:jpa-spring-data:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/SafeSortField.java' 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/SafeSortRegistry.java' 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/SafeSortMapper.java' 'modules/jpa/jpa-spring-data/src/test/java/io/backend/skeleton/jpa/springdata/SafeSortMapperTest.java'
git commit -m "feat: enforce allowlisted deterministic jpa sorting"
Task 22: Hibernate Statement Inspector와 Statistics Snapshot 구현
Files:
- Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/QueryNameContext.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/NamedStatementInspector.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/HibernateStatisticsSnapshot.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/HibernateStatisticsCollector.java - Test:
modules/jpa/jpa-hibernate/src/test/java/io/backend/skeleton/jpa/hibernate/HibernateStatisticsCollectorTest.java
Interfaces:
- Consumes: Hibernate 7.4 StatementInspector/Statistics and
QueryName. - Produces: Per-scope statement, entity, collection, flush and batch statistics without SQL parameter capture.
Implementation requirements:
-
Use query-name comments or context metadata without including dynamic values.
-
Snapshot entity load/fetch and collection load/fetch separately.
-
Record prepared statement count, flush count and JDBC batch execution count.
-
Clear query context in finally blocks.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.hibernate;
class HibernateStatisticsCollectorTest {
@Test
void separatesEntityLoadFromEntityFetch() {
var before = collector.snapshot();
fixture.loadOrdersWithSharedUser();
var delta = collector.snapshot().minus(before);
assertThat(delta.entityLoadCount()).isPositive();
assertThat(delta.entityFetchCount()).isGreaterThanOrEqualTo(0);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-hibernate:test --tests 'io.backend.skeleton.jpa.hibernate.HibernateStatisticsCollectorTest'
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.hibernate;
public record HibernateStatisticsSnapshot(
long preparedStatements,
long entityLoads,
long entityFetches,
long collectionLoads,
long collectionFetches,
long flushes,
long jdbcBatches) {
public HibernateStatisticsSnapshot minus(HibernateStatisticsSnapshot before) {
return new HibernateStatisticsSnapshot(
preparedStatements - before.preparedStatements,
entityLoads - before.entityLoads,
entityFetches - before.entityFetches,
collectionLoads - before.collectionLoads,
collectionFetches - before.collectionFetches,
flushes - before.flushes,
jdbcBatches - before.jdbcBatches);
}
}
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:jpa-hibernate:test --tests 'io.backend.skeleton.jpa.hibernate.HibernateStatisticsCollectorTest'
./gradlew :modules:jpa:jpa-hibernate:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/QueryNameContext.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/NamedStatementInspector.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/HibernateStatisticsSnapshot.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/HibernateStatisticsCollector.java' 'modules/jpa/jpa-hibernate/src/test/java/io/backend/skeleton/jpa/hibernate/HibernateStatisticsCollectorTest.java'
git commit -m "feat: collect hibernate query and fetch statistics"
Task 23: Query Count·N+1 Assertion Testkit 구현
Files:
- Create:
modules/jpa/jpa-testkit/src/main/java/io/backend/skeleton/jpa/testkit/query/QueryExpectation.java - Create:
modules/jpa/jpa-testkit/src/main/java/io/backend/skeleton/jpa/testkit/query/FetchExpectation.java - Create:
modules/jpa/jpa-testkit/src/main/java/io/backend/skeleton/jpa/testkit/query/JpaQueryAssertions.java - Create:
modules/jpa/jpa-testkit/src/main/java/io/backend/skeleton/jpa/testkit/query/QueryMeasurement.java - Test:
modules/jpa/jpa-testkit/src/test/java/io/backend/skeleton/jpa/testkit/query/JpaQueryAssertionsTest.java
Interfaces:
- Consumes: Task 22 statistics snapshots and a statement/row measurement adapter.
- Produces: Assertions for statement count, fetch count, hydrated entities, rows and bounded execution time.
Implementation requirements:
-
Do not reduce N+1 verification to statement count only.
-
Allow upper bounds and exact expectations separately.
-
Error output must show queryName and each measured dimension.
-
Support skewed and shared-association fixtures in PG contract suites.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.testkit.query;
class JpaQueryAssertionsTest {
@Test
void reportsCartesianAmplificationEvenForOneStatement() {
var measurement = new QueryMeasurement(1, 100, 2000, 2000, Duration.ofMillis(40));
assertThatThrownBy(() -> assertions.assertMatches(
measurement, QueryExpectation.maxRows(500)))
.hasMessageContaining("rows=2000");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-testkit:test --tests 'io.backend.skeleton.jpa.testkit.query.JpaQueryAssertionsTest'
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.testkit.query;
public final class JpaQueryAssertions {
public void assertMatches(
QueryMeasurement actual,
QueryExpectation expected) {
if (!expected.matches(actual)) {
throw new AssertionError("JPA query expectation failed: " + actual.summary());
}
}
}
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:jpa-testkit:test --tests 'io.backend.skeleton.jpa.testkit.query.JpaQueryAssertionsTest'
./gradlew :modules:jpa:jpa-testkit:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-testkit/src/main/java/io/backend/skeleton/jpa/testkit/query/QueryExpectation.java' 'modules/jpa/jpa-testkit/src/main/java/io/backend/skeleton/jpa/testkit/query/FetchExpectation.java' 'modules/jpa/jpa-testkit/src/main/java/io/backend/skeleton/jpa/testkit/query/JpaQueryAssertions.java' 'modules/jpa/jpa-testkit/src/main/java/io/backend/skeleton/jpa/testkit/query/QueryMeasurement.java' 'modules/jpa/jpa-testkit/src/test/java/io/backend/skeleton/jpa/testkit/query/JpaQueryAssertionsTest.java'
git commit -m "test: add quantitative jpa query assertions"
Task 24: Use Case Fetch Plan과 EntityGraph Helper 구현
Files:
- Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/FetchPlanName.java - Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/EntityGraphCatalog.java - Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/FetchPlanApplier.java - Test:
modules/jpa/jpa-spring-data/src/test/java/io/backend/skeleton/jpa/springdata/FetchPlanApplierTest.java
Interfaces:
- Consumes: EntityManager graphs, registered QueryName and domain-defined graph names.
- Produces: Use-case-specific EntityGraph selection without changing mapping fetch defaults.
Implementation requirements:
-
Require a registered fetch-plan name; no arbitrary attribute strings from API input.
-
Support fetchgraph and loadgraph semantics explicitly.
-
Do not mutate global Entity mapping or turn associations EAGER.
-
Expose applied fetch plan to observation context.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.springdata;
class FetchPlanApplierTest {
@Test
void appliesRegisteredGraphAndRejectsUnknownGraph() {
var query = fixtureQuery();
applier.apply(query, new FetchPlanName("order.detail"));
assertThat(query.getHints()).containsKey("jakarta.persistence.fetchgraph");
assertThatThrownBy(() -> applier.apply(query, new FetchPlanName("order.secret")))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-spring-data:test --tests 'io.backend.skeleton.jpa.springdata.FetchPlanApplierTest'
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.springdata;
public final class FetchPlanApplier {
public <T> TypedQuery<T> apply(TypedQuery<T> query, FetchPlanName name) {
EntityGraph<?> graph = catalog.require(name);
return query.setHint("jakarta.persistence.fetchgraph", graph);
}
}
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:jpa-spring-data:test --tests 'io.backend.skeleton.jpa.springdata.FetchPlanApplierTest'
./gradlew :modules:jpa:jpa-spring-data:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/FetchPlanName.java' 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/EntityGraphCatalog.java' 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/FetchPlanApplier.java' 'modules/jpa/jpa-spring-data/src/test/java/io/backend/skeleton/jpa/springdata/FetchPlanApplierTest.java'
git commit -m "feat: add use case specific entity graph support"
Task 25: Hibernate 7.4 Collection Fetch Pagination 회귀 Suite 구현
Files:
- Create:
modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/fetch/PagedParent.java - Create:
modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/fetch/PagedChild.java - Create:
modules/jpa/jpa-testkit-postgresql/src/compatibilityTest/java/io/backend/skeleton/jpa/testkit/fetch/HibernateCollectionFetchPaginationContractTest.java - Test:
modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/fetch/FetchPaginationExpectationTest.java
Interfaces:
- Consumes: Hibernate 7.4, PG16·17·18, Task 23 measurement and a parent/child skew fixture.
- Produces: A version-specific gate for SQL limit/subquery behavior, parent count, row amplification and count correctness.
Implementation requirements:
-
Test one fetched collection with Page and exact parent limit.
-
Capture generated SQL and prove DB-level bounded selection under Hibernate 7.4.
-
Keep a negative multiple-collection Cartesian test.
-
Run on all Stable PostgreSQL versions and every Boot/Hibernate patch upgrade.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.testkit.fetch;
class FetchPaginationExpectationTest {
@Test
void oneCollectionPageRequiresBoundedParentSelection() {
var expected = FetchPaginationExpectation.hibernate74PostgreSql(20);
assertThat(expected.maxReturnedParents()).isEqualTo(20);
assertThat(expected.requiresDatabaseLimit()).isTrue();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.fetch.FetchPaginationExpectationTest'
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.testkit.fetch;
public record FetchPaginationExpectation(
int maxReturnedParents,
boolean requiresDatabaseLimit,
int maxRowAmplification) {
public static FetchPaginationExpectation hibernate74PostgreSql(int pageSize) {
return new FetchPaginationExpectation(pageSize, true, pageSize * 100);
}
}
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:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.fetch.FetchPaginationExpectationTest'
./gradlew :modules:jpa:jpa-testkit-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/fetch/PagedParent.java' 'modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/fetch/PagedChild.java' 'modules/jpa/jpa-testkit-postgresql/src/compatibilityTest/java/io/backend/skeleton/jpa/testkit/fetch/HibernateCollectionFetchPaginationContractTest.java' 'modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/fetch/FetchPaginationExpectationTest.java'
git commit -m "test: certify hibernate collection fetch pagination"
Task 26: Keyset Pagination Core Cursor 계약 구현
Files:
- Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/SortDirection.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/KeysetPageRequest.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/KeysetSlice.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/CursorCodec.java - Create:
modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/SignedJsonCursorCodec.java - Test:
modules/jpa/jpa-core-api/src/test/java/io/backend/skeleton/jpa/api/query/SignedJsonCursorCodecTest.java
Interfaces:
- Consumes: Java JSON codec adapter and an application-provided HMAC key.
- Produces: Versioned, bounded, tamper-evident cursor API independent of Spring Data.
Implementation requirements:
-
Require page size between 1 and a configured maximum.
-
Cursor payload includes version and all ordering tie-breakers.
-
Do not place JPQL, SQL fragments or raw entity paths in cursor data.
-
Reject signature mismatch and unknown cursor version.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.api.query;
class SignedJsonCursorCodecTest {
@Test
void detectsTamperingAndRoundTripsTieBreaker() {
var cursor = new OrderCursor(Instant.parse("2026-08-11T00:00:00Z"), UUID.randomUUID());
var encoded = codec.encode(cursor);
assertThat(codec.decode(encoded)).isEqualTo(cursor);
assertThatThrownBy(() -> codec.decode(encoded + "x"))
.isInstanceOf(IllegalArgumentException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-core-api:test --tests 'io.backend.skeleton.jpa.api.query.SignedJsonCursorCodecTest'
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.api.query;
public record KeysetPageRequest<C>(
Optional<C> after,
int size,
SortDirection direction) {
public KeysetPageRequest {
if (size < 1 || size > 500) throw new IllegalArgumentException("invalid page size");
}
}
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:jpa-core-api:test --tests 'io.backend.skeleton.jpa.api.query.SignedJsonCursorCodecTest'
./gradlew :modules:jpa:jpa-core-api:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/SortDirection.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/KeysetPageRequest.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/KeysetSlice.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/CursorCodec.java' 'modules/jpa/jpa-core-api/src/main/java/io/backend/skeleton/jpa/api/query/SignedJsonCursorCodec.java' 'modules/jpa/jpa-core-api/src/test/java/io/backend/skeleton/jpa/api/query/SignedJsonCursorCodecTest.java'
git commit -m "feat: add signed keyset cursor contracts"
Task 27: Spring Data Keyset Query Support 구현
Files:
- Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/JpaKeysetQuerySupport.java - Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/KeysetPredicateBuilder.java - Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/KeysetSliceAssembler.java - Test:
modules/jpa/jpa-spring-data/src/test/java/io/backend/skeleton/jpa/springdata/JpaKeysetQuerySupportTest.java
Interfaces:
- Consumes: Task 26 cursor types, Criteria API and domain-provided keyset adapters.
- Produces: Deterministic size+1 keyset query execution and next-cursor assembly.
Implementation requirements:
-
Use lexicographic predicates matching the exact sort direction and null policy.
-
Require a unique tie-breaker.
-
Fetch at most
size + 1rows and return onlysize. -
Do not execute a count query.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.springdata;
class JpaKeysetQuerySupportTest {
@Test
void duplicateCreatedAtUsesIdTieBreakerWithoutGap() {
var first = repository.findRecent(request(Optional.empty(), 2));
var second = repository.findRecent(request(first.nextCursor(), 2));
assertThat(Stream.concat(first.items().stream(), second.items().stream()))
.extracting(OrderSummary::id)
.doesNotHaveDuplicates();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-spring-data:test --tests 'io.backend.skeleton.jpa.springdata.JpaKeysetQuerySupportTest'
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.springdata;
public final class KeysetSliceAssembler {
public <T, C> KeysetSlice<T, C> assemble(
List<T> fetched,
int requestedSize,
Function<T, C> cursorExtractor) {
boolean hasNext = fetched.size() > requestedSize;
List<T> items = List.copyOf(fetched.subList(0, Math.min(fetched.size(), requestedSize)));
Optional<C> next = hasNext ? Optional.of(cursorExtractor.apply(items.getLast())) : Optional.empty();
return new KeysetSlice<>(items, next, hasNext);
}
}
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:jpa-spring-data:test --tests 'io.backend.skeleton.jpa.springdata.JpaKeysetQuerySupportTest'
./gradlew :modules:jpa:jpa-spring-data:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/JpaKeysetQuerySupport.java' 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/KeysetPredicateBuilder.java' 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/KeysetSliceAssembler.java' 'modules/jpa/jpa-spring-data/src/test/java/io/backend/skeleton/jpa/springdata/JpaKeysetQuerySupportTest.java'
git commit -m "feat: implement deterministic jpa keyset pagination"
Task 28: Scroll·Stream Resource Guard 구현
Files:
- Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/JpaStreamScope.java - Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/JpaStreamExecutor.java - Create:
modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/ScrollPolicy.java - Test:
modules/jpa/jpa-spring-data/src/test/java/io/backend/skeleton/jpa/springdata/JpaStreamExecutorTest.java
Interfaces:
- Consumes: Spring Data Scroll/Stream APIs, Transaction synchronization and QueryName.
- Produces: A bounded resource scope that closes Stream/ResultSet and forbids returning it beyond the transaction.
Implementation requirements:
-
Require an active read-only transaction for stream execution.
-
Close the stream in normal, exception and cancellation paths.
-
Require fetch size, maximum rows or explicit admin token.
-
Reject WebFlux/Reactor return types in this blocking module.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.springdata;
class JpaStreamExecutorTest {
@Test
void closesStreamWhenConsumerFails() {
assertThatThrownBy(() -> executor.consume(QUERY, policy(100), stream -> {
stream.findFirst();
throw new IllegalStateException("boom");
})).isInstanceOf(IllegalStateException.class);
assertThat(resourceProbe.closed()).isTrue();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-spring-data:test --tests 'io.backend.skeleton.jpa.springdata.JpaStreamExecutorTest'
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.springdata;
public final class JpaStreamExecutor {
public <T, R> R consume(
QueryName query,
ScrollPolicy policy,
Supplier<Stream<T>> supplier,
Function<Stream<T>, R> consumer) {
TransactionGuard.requireActiveReadOnly();
try (Stream<T> stream = supplier.get()) {
return consumer.apply(stream.limit(policy.maxRows()));
}
}
}
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:jpa-spring-data:test --tests 'io.backend.skeleton.jpa.springdata.JpaStreamExecutorTest'
./gradlew :modules:jpa:jpa-spring-data:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/JpaStreamScope.java' 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/JpaStreamExecutor.java' 'modules/jpa/jpa-spring-data/src/main/java/io/backend/skeleton/jpa/springdata/ScrollPolicy.java' 'modules/jpa/jpa-spring-data/src/test/java/io/backend/skeleton/jpa/springdata/JpaStreamExecutorTest.java'
git commit -m "feat: guard jpa scroll and stream resources"
Task 29: Optimistic Lock 오류 변환과 전체 Use Case Retry 계약 구현
Files:
- Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/OptimisticConflictTranslator.java - Create:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/DefaultJpaRetryPolicy.java - Modify:
modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/FullTransactionRetryCoordinator.java - Test:
modules/jpa/jpa-transaction/src/integrationTest/java/io/backend/skeleton/jpa/transaction/OptimisticRetryIntegrationTest.java
Interfaces:
- Consumes: JPA
OptimisticLockException, Spring optimistic locking exceptions and Task 8 coordinator. - Produces: Stable
OptimisticConflictExceptionand bounded full-transaction recomputation.
Implementation requirements:
-
Translate conflicts thrown at flush or commit.
-
Ensure retry reloads the entity and reruns domain rules.
-
Do not retry when the use case declared external irreversible side effects.
-
Record conflict entity type only from a bounded catalog, never Entity ID.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.transaction;
class OptimisticRetryIntegrationTest {
@Test
void secondAttemptReloadsAndRecomputesAggregate() {
concurrentWriterUpdatesVersion();
var result = retryingService.increaseQuantity(orderId, 2);
assertThat(result.attempts()).isEqualTo(2);
assertThat(repository.findById(orderId).orElseThrow().quantity()).isEqualTo(5);
assertThat(probe.persistenceContextIds()).doesNotHaveDuplicates();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-transaction:integrationTest --tests 'io.backend.skeleton.jpa.transaction.OptimisticRetryIntegrationTest'
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.transaction;
public final class DefaultJpaRetryPolicy implements JpaRetryPolicy {
public RetryDecision classify(
JpaPersistenceException failure,
TransactionAttempt attempt) {
if (failure instanceof TransactionCompletionUnknownException) {
return RetryDecision.reconcile("transaction completion is unknown");
}
if (failure instanceof OptimisticConflictException ||
failure instanceof SerializationFailureException ||
failure instanceof DeadlockDetectedException) {
return RetryDecision.retry(backoff.forAttempt(attempt.number()));
}
return RetryDecision.fail("non-retryable persistence failure");
}
}
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:jpa-transaction:integrationTest --tests 'io.backend.skeleton.jpa.transaction.OptimisticRetryIntegrationTest'
./gradlew :modules:jpa:jpa-transaction:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/OptimisticConflictTranslator.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/DefaultJpaRetryPolicy.java' 'modules/jpa/jpa-transaction/src/main/java/io/backend/skeleton/jpa/transaction/FullTransactionRetryCoordinator.java' 'modules/jpa/jpa-transaction/src/integrationTest/java/io/backend/skeleton/jpa/transaction/OptimisticRetryIntegrationTest.java'
git commit -m "feat: retry optimistic conflicts as complete transactions"
Task 30: Pessimistic Lock Timeout과 Deadlock 변환 구현
Files:
- Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/PostgreSqlLockOptions.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/PostgreSqlLockExceptionTranslator.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/LockWaitObservation.java - Test:
modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/lock/PostgreSqlPessimisticLockContractTest.java
Interfaces:
- Consumes: JPA Pessimistic lock hints, SQLSTATE classifier and PostgreSQL Testcontainers.
- Produces: Distinct lock-timeout, NOWAIT and deadlock errors with lock-wait metrics.
Implementation requirements:
-
Distinguish statement-level lock timeout from transaction-aborting deadlock.
-
Map
55P03to lock-not-available/timeout and40P01to deadlock. -
Require finite lock timeout for pessimistic lock profiles.
-
Hold locks only inside the Application Transaction.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.postgresql.lock;
class PostgreSqlPessimisticLockContractTest {
@Test
void nowaitFailsImmediatelyWhileBlockingLockTimesOutSeparately() {
lockRowInOtherTransaction();
assertThatThrownBy(() -> repository.findForUpdateNowait(id))
.isInstanceOf(PessimisticLockTimeoutException.class);
assertThat(lockProbe.lastWait()).isLessThan(Duration.ofSeconds(1));
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.lock.PostgreSqlPessimisticLockContractTest'
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.postgresql.lock;
public record PostgreSqlLockOptions(
LockModeType mode,
Duration timeout,
boolean nowait) {
public PostgreSqlLockOptions {
if (timeout == null || timeout.isNegative()) {
throw new IllegalArgumentException("lock timeout must be finite");
}
}
}
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:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.lock.PostgreSqlPessimisticLockContractTest'
./gradlew :modules:jpa:jpa-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/PostgreSqlLockOptions.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/PostgreSqlLockExceptionTranslator.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/LockWaitObservation.java' 'modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/lock/PostgreSqlPessimisticLockContractTest.java'
git commit -m "feat: classify postgresql pessimistic lock failures"
Task 31: PostgreSQL NOWAIT·SKIP LOCKED Work Claim Extension 구현
Files:
- Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/WorkQueueName.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/WorkClaimExecutor.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/PostgreSqlWorkClaimExecutor.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/WorkClaim.java - Test:
modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/lock/PostgreSqlWorkClaimContractTest.java
Interfaces:
- Consumes: EntityManager native query, registered queue SQL and PostgreSQL
FOR UPDATE SKIP LOCKED. - Produces: Queue-specific batch claim semantics instead of a generic inconsistent-read API.
Implementation requirements:
-
Require a registered queue name and fixed SQL template.
-
Claim rows in deterministic priority/id order.
-
Return lease owner and lease-until evidence in the same transaction.
-
Do not expose
skipLocked=trueon arbitrary repository methods. -
Step 1: Write the failing test
package io.backend.skeleton.jpa.postgresql.lock;
class PostgreSqlWorkClaimContractTest {
@Test
void competingWorkersClaimDisjointRows() {
var first = workerA.claimNextBatch(QUEUE, 10, Duration.ofMinutes(1));
var second = workerB.claimNextBatch(QUEUE, 10, Duration.ofMinutes(1));
assertThat(first).extracting(WorkClaim::id)
.doesNotContainAnyElementsOf(second.stream().map(WorkClaim::id).toList());
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.lock.PostgreSqlWorkClaimContractTest'
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.postgresql.lock;
public interface WorkClaimExecutor<T, K> {
List<WorkClaim<T, K>> claimNextBatch(
WorkQueueName queue,
int size,
Duration lease);
}
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:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.lock.PostgreSqlWorkClaimContractTest'
./gradlew :modules:jpa:jpa-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/WorkQueueName.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/WorkClaimExecutor.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/PostgreSqlWorkClaimExecutor.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/lock/WorkClaim.java' 'modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/lock/PostgreSqlWorkClaimContractTest.java'
git commit -m "feat: add postgresql skip locked work claims"
Task 32: Constraint Violation Catalog와 Race-safe 오류 변환 구현
Files:
- Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/constraint/ConstraintCode.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/constraint/PostgreSqlConstraintCatalog.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java - Modify:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/error/PostgreSqlExceptionTranslator.java - Test:
modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/constraint/ConstraintRaceContractTest.java
Interfaces:
- Consumes: Structured PostgreSQL server error fields and design-time constraint registry.
- Produces: Stable application constraint codes for unique, foreign-key, not-null and check violations.
Implementation requirements:
-
Two concurrent inserts of the same logical key must result in one commit and one unique exception.
-
Do not rely on a prior
existsquery for correctness. -
Unknown constraint names map to a generic bounded code and secure diagnostic metadata.
-
Support partial unique index and
NULLS NOT DISTINCTmigration names. -
Step 1: Write the failing test
package io.backend.skeleton.jpa.postgresql.constraint;
class ConstraintRaceContractTest {
@Test
void concurrentCreateIsResolvedByDatabaseConstraint() {
var results = runConcurrently(
() -> service.create("same@example.test"),
() -> service.create("same@example.test"));
assertThat(results.successCount()).isEqualTo(1);
assertThat(results.failure()).isInstanceOf(UniqueConstraintViolationException.class);
assertThat(((UniqueConstraintViolationException) results.failure())
.details().code()).isEqualTo(new ConstraintCode("user.active-email.unique"));
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.constraint.ConstraintRaceContractTest'
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.postgresql.constraint;
public final class PostgreSqlConstraintCatalog {
private final Map<String, ConstraintCode> byDatabaseName;
public ConstraintCode resolve(String databaseName) {
return byDatabaseName.getOrDefault(
databaseName, new ConstraintCode("database.constraint.unknown"));
}
}
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:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.constraint.ConstraintRaceContractTest'
./gradlew :modules:jpa:jpa-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/constraint/ConstraintCode.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/constraint/PostgreSqlConstraintCatalog.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/error/PostgreSqlExceptionTranslator.java' 'modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/constraint/ConstraintRaceContractTest.java'
git commit -m "feat: map database constraints to stable error codes"
Task 33: Hibernate JDBC Batch Profile과 Configuration Guard 구현
Files:
- Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/JpaBatchProfile.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/JpaBatchProfileRegistry.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/HibernateBatchConfigurationGuard.java - Test:
modules/jpa/jpa-hibernate/src/test/java/io/backend/skeleton/jpa/hibernate/batch/HibernateBatchConfigurationGuardTest.java
Interfaces:
- Consumes: Hibernate batch settings and Entity identifier metadata.
- Produces: Named batch profiles and startup diagnostics for IDENTITY and sequence mismatch.
Implementation requirements:
-
Require positive batch, flush and clear sizes for enabled profiles.
-
Warn/fail when a write-heavy batch profile targets IDENTITY entities.
-
Validate sequence allocation size against migration metadata in the contract suite.
-
Treat
order_insertsandorder_updatesas profile options, not universal defaults. -
Step 1: Write the failing test
package io.backend.skeleton.jpa.hibernate.batch;
class HibernateBatchConfigurationGuardTest {
@Test
void rejectsIdentityEntityInRequiredBatchProfile() {
var profile = new JpaBatchProfile("import", 50, 50, 50, true, true, true);
assertThatThrownBy(() -> guard.validate(profile, IdentityEntity.class))
.hasMessageContaining("IDENTITY disables insert batching");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-hibernate:test --tests 'io.backend.skeleton.jpa.hibernate.batch.HibernateBatchConfigurationGuardTest'
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.hibernate.batch;
public record JpaBatchProfile(
String name,
int jdbcBatchSize,
int flushSize,
int clearSize,
boolean orderInserts,
boolean orderUpdates,
boolean batchingRequired) {
public JpaBatchProfile {
if (jdbcBatchSize < 1 || flushSize < 1 || clearSize < 1) {
throw new IllegalArgumentException("batch sizes must be positive");
}
}
}
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:jpa-hibernate:test --tests 'io.backend.skeleton.jpa.hibernate.batch.HibernateBatchConfigurationGuardTest'
./gradlew :modules:jpa:jpa-hibernate:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/JpaBatchProfile.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/JpaBatchProfileRegistry.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/HibernateBatchConfigurationGuard.java' 'modules/jpa/jpa-hibernate/src/test/java/io/backend/skeleton/jpa/hibernate/batch/HibernateBatchConfigurationGuardTest.java'
git commit -m "feat: define verified hibernate batch profiles"
Task 34: Chunked Batch Persist Executor 구현
Files:
- Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/JpaBatchExecutor.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/HibernateJpaBatchExecutor.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/BatchExecutionResult.java - Test:
modules/jpa/jpa-hibernate/src/integrationTest/java/io/backend/skeleton/jpa/hibernate/batch/HibernateJpaBatchExecutorIntegrationTest.java
Interfaces:
- Consumes: Task 33 profile, EntityManager and Hibernate statistics.
- Produces: Flush/clear bounded batch persistence with measured JDBC batch execution.
Implementation requirements:
-
Persist each item exactly once inside a caller-owned transaction.
-
Flush and clear at configured boundaries and once at the end.
-
Reject a Stream that cannot report or enforce a maximum input count unless admin capability is present.
-
Return processed rows, flush count, statement count and actual batch count.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.hibernate.batch;
class HibernateJpaBatchExecutorIntegrationTest {
@Test
void executesActualJdbcBatchesAndBoundsPersistenceContext() {
var result = executor.persist(BATCH_PROFILE, fixtures(1_000), entityManager::persist);
assertThat(result.processed()).isEqualTo(1_000);
assertThat(result.jdbcBatches()).isGreaterThan(1);
assertThat(result.maxManagedEntities()).isLessThanOrEqualTo(50);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-hibernate:integrationTest --tests 'io.backend.skeleton.jpa.hibernate.batch.HibernateJpaBatchExecutorIntegrationTest'
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.hibernate.batch;
public final class HibernateJpaBatchExecutor implements JpaBatchExecutor {
public <T> BatchExecutionResult persist(
JpaBatchProfile profile,
Iterable<T> items,
Consumer<T> persister) {
int processed = 0;
for (T item : items) {
persister.accept(item);
processed++;
if (processed % profile.flushSize() == 0) {
entityManager.flush();
entityManager.clear();
}
}
entityManager.flush();
entityManager.clear();
return measurements.result(processed);
}
}
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:jpa-hibernate:integrationTest --tests 'io.backend.skeleton.jpa.hibernate.batch.HibernateJpaBatchExecutorIntegrationTest'
./gradlew :modules:jpa:jpa-hibernate:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/JpaBatchExecutor.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/HibernateJpaBatchExecutor.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/batch/BatchExecutionResult.java' 'modules/jpa/jpa-hibernate/src/integrationTest/java/io/backend/skeleton/jpa/hibernate/batch/HibernateJpaBatchExecutorIntegrationTest.java'
git commit -m "feat: execute bounded hibernate jdbc batches"
Task 35: Bulk DML flush-clear Executor 구현
Files:
- Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/bulk/BulkOperationName.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/bulk/BulkDmlExecutor.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/bulk/HibernateBulkDmlExecutor.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/bulk/BulkDmlResult.java - Test:
modules/jpa/jpa-hibernate/src/integrationTest/java/io/backend/skeleton/jpa/hibernate/bulk/HibernateBulkDmlExecutorIntegrationTest.java
Interfaces:
- Consumes: EntityManager, registered bulk operation and Task 18 QueryObservation.
- Produces: Explicit flush → bulk SQL → clear execution with affected-row guard.
Implementation requirements:
-
Require an active transaction and registered operation name.
-
Flush before query execution and clear immediately after it.
-
Require minimum/maximum expected affected rows; fail on unexpected blast radius.
-
Document that callbacks and optimistic version checks are bypassed.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.hibernate.bulk;
class HibernateBulkDmlExecutorIntegrationTest {
@Test
void clearsStaleManagedEntitiesAfterBulkUpdate() {
var managed = repository.findById(id).orElseThrow();
executor.execute(OPERATION, () -> query.executeUpdate(), expectedRows(1));
assertThat(entityManager.contains(managed)).isFalse();
assertThat(repository.findById(id).orElseThrow().status()).isEqualTo("ARCHIVED");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-hibernate:integrationTest --tests 'io.backend.skeleton.jpa.hibernate.bulk.HibernateBulkDmlExecutorIntegrationTest'
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.hibernate.bulk;
public final class HibernateBulkDmlExecutor implements BulkDmlExecutor {
public BulkDmlResult execute(
BulkOperationName name,
IntSupplier statement,
AffectedRowsExpectation expectation) {
entityManager.flush();
int affected = statement.getAsInt();
entityManager.clear();
expectation.verify(affected);
return new BulkDmlResult(name, affected);
}
}
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:jpa-hibernate:integrationTest --tests 'io.backend.skeleton.jpa.hibernate.bulk.HibernateBulkDmlExecutorIntegrationTest'
./gradlew :modules:jpa:jpa-hibernate:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/bulk/BulkOperationName.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/bulk/BulkDmlExecutor.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/bulk/HibernateBulkDmlExecutor.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/bulk/BulkDmlResult.java' 'modules/jpa/jpa-hibernate/src/integrationTest/java/io/backend/skeleton/jpa/hibernate/bulk/HibernateBulkDmlExecutorIntegrationTest.java'
git commit -m "feat: execute safe jpa bulk dml with context clearing"
Task 36: Hibernate StatelessSession Advanced Runner 구현
Files:
- Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/stateless/StatelessWorkName.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/stateless/StatelessSessionRunner.java - Create:
modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/stateless/HibernateStatelessSessionRunner.java - Test:
modules/jpa/jpa-hibernate/src/integrationTest/java/io/backend/skeleton/jpa/hibernate/stateless/HibernateStatelessSessionRunnerIntegrationTest.java
Interfaces:
- Consumes: Hibernate SessionFactory and J4/Advanced authorization token.
- Produces: An opt-in bulk session with explicit no-dirty-checking/no-cascade semantics.
Implementation requirements:
-
Do not register this runner as the default Repository implementation.
-
Require a named operation, row cap and explicit transaction mode.
-
Document that returned objects are not managed and aliases may occur.
-
Measure rows, statements and memory independent of persistence-context size.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.hibernate.stateless;
class HibernateStatelessSessionRunnerIntegrationTest {
@Test
void insertsWithoutGrowingPersistenceContext() {
var result = runner.execute(WORK, 10_000, session -> {
fixtures(10_000).forEach(session::insert);
return 10_000;
});
assertThat(result).isEqualTo(10_000);
assertThat(hibernateSessionStatistics.managedEntityCount()).isZero();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-hibernate:integrationTest --tests 'io.backend.skeleton.jpa.hibernate.stateless.HibernateStatelessSessionRunnerIntegrationTest'
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.hibernate.stateless;
public final class HibernateStatelessSessionRunner implements StatelessSessionRunner {
public <T> T execute(
StatelessWorkName name,
long maxRows,
Function<StatelessSession, T> work) {
try (StatelessSession session = sessionFactory.openStatelessSession()) {
Transaction tx = session.beginTransaction();
try {
T result = work.apply(session);
tx.commit();
return result;
} catch (RuntimeException failure) {
tx.rollback();
throw failure;
}
}
}
}
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:jpa-hibernate:integrationTest --tests 'io.backend.skeleton.jpa.hibernate.stateless.HibernateStatelessSessionRunnerIntegrationTest'
./gradlew :modules:jpa:jpa-hibernate:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/stateless/StatelessWorkName.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/stateless/StatelessSessionRunner.java' 'modules/jpa/jpa-hibernate/src/main/java/io/backend/skeleton/jpa/hibernate/stateless/HibernateStatelessSessionRunner.java' 'modules/jpa/jpa-hibernate/src/integrationTest/java/io/backend/skeleton/jpa/hibernate/stateless/HibernateStatelessSessionRunnerIntegrationTest.java'
git commit -m "feat: add opt in hibernate stateless session runner"
Task 37: PostgreSQL JSONB Mapping과 Query Contract 구현
Files:
- Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/json/JsonDocument.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/json/JsonDocumentCodec.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/json/PostgreSqlJsonQuerySupport.java - Test:
modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/json/PostgreSqlJsonbContractTest.java
Interfaces:
- Consumes: Hibernate JSON JDBC type, Jackson adapter and PostgreSQL JSONB operators.
- Produces: Versioned JSONB value mapping and parameter-bound JSON path/containment queries.
Implementation requirements:
-
Do not store Java class names in JSON payload.
-
Require schema name/version in
JsonDocument. -
Use parameters for values and a registered catalog for JSON paths.
-
Test GIN index plan separately in Task 44.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.postgresql.json;
class PostgreSqlJsonbContractTest {
@Test
void roundTripsVersionedDocumentAndQueriesByRegisteredPath() {
repository.save(entity(json("profile", 2, Map.of("tier", "pro"))));
entityManager.flush();
assertThat(querySupport.contains(PATH_TIER, "pro"))
.extracting(Result::schemaVersion)
.containsExactly(2);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.json.PostgreSqlJsonbContractTest'
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.postgresql.json;
public record JsonDocument(
String schema,
int version,
JsonNode payload) {
public JsonDocument {
if (schema == null || schema.isBlank() || version < 1) {
throw new IllegalArgumentException("invalid json document envelope");
}
}
}
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:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.json.PostgreSqlJsonbContractTest'
./gradlew :modules:jpa:jpa-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/json/JsonDocument.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/json/JsonDocumentCodec.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/json/PostgreSqlJsonQuerySupport.java' 'modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/json/PostgreSqlJsonbContractTest.java'
git commit -m "feat: add postgresql jsonb persistence support"
Task 38: PostgreSQL Array·Range Mapping Contract 구현
Files:
- Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/array/PostgreSqlArraySupport.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/range/PgRange.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/range/PgRangeJdbcType.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/range/PostgreSqlRangeQuerySupport.java - Test:
modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/range/PostgreSqlArrayRangeContractTest.java
Interfaces:
- Consumes: Hibernate JDBC type SPI and PostgreSQL array/range types.
- Produces: Typed array and bounded/unbounded range round-trip and overlap/containment query support.
Implementation requirements:
-
Represent open/closed and unbounded endpoints explicitly.
-
Reject invalid ranges in Java before sending them.
-
Do not flatten ranges into two unrelated columns in this extension.
-
Run identical contracts on PG16·17·18.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.postgresql.range;
class PostgreSqlArrayRangeContractTest {
@Test
void roundTripsClosedOpenRangeAndArray() {
var saved = repository.save(fixture(
List.of("a", "b"), PgRange.closedOpen(Instant.EPOCH, Instant.EPOCH.plusSeconds(60))));
entityManager.flush();
entityManager.clear();
var loaded = repository.findById(saved.id()).orElseThrow();
assertThat(loaded.tags()).containsExactly("a", "b");
assertThat(loaded.window().upperInclusive()).isFalse();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.range.PostgreSqlArrayRangeContractTest'
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.postgresql.range;
public record PgRange<T extends Comparable<T>>(
Optional<T> lower,
boolean lowerInclusive,
Optional<T> upper,
boolean upperInclusive) {
public PgRange {
if (lower.isPresent() && upper.isPresent() &&
lower.get().compareTo(upper.get()) > 0) {
throw new IllegalArgumentException("range lower bound exceeds upper bound");
}
}
}
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:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.range.PostgreSqlArrayRangeContractTest'
./gradlew :modules:jpa:jpa-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/array/PostgreSqlArraySupport.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/range/PgRange.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/range/PgRangeJdbcType.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/range/PostgreSqlRangeQuerySupport.java' 'modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/range/PostgreSqlArrayRangeContractTest.java'
git commit -m "feat: add postgresql array and range mappings"
Task 39: PostgreSQL ON CONFLICT·RETURNING Native Write 구현
Files:
- Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/write/NativeWriteName.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/write/UpsertConflictTarget.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/write/PostgreSqlUpsertExecutor.java - Create:
modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/write/RegisteredPostgreSqlUpsertExecutor.java - Test:
modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/write/PostgreSqlUpsertContractTest.java
Interfaces:
- Consumes: Registered native SQL, parameter binder, QueryObservation and Persistence Context clear policy.
- Produces: Explicit upsert result with inserted/updated disposition and returned projection.
Implementation requirements:
-
Require a registered conflict target and fixed update column set.
-
Parameter-bind all values; dynamic table/column names are forbidden.
-
Return whether insert or conflict-update occurred when SQL can expose it.
-
Clear or refresh affected managed Entity state before returning to JPA code.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.postgresql.write;
class PostgreSqlUpsertContractTest {
@Test
void concurrentUpsertReturnsOneLogicalRow() {
runConcurrently(
() -> executor.execute(UPSERT, command("key", 1)),
() -> executor.execute(UPSERT, command("key", 2)));
assertThat(jdbc.queryForObject("select count(*) from counters where key='key'", Long.class))
.isEqualTo(1L);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.write.PostgreSqlUpsertContractTest'
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.postgresql.write;
public interface PostgreSqlUpsertExecutor<C, R> {
UpsertResult<R> execute(NativeWriteName operation, C command);
}
public record UpsertResult<R>(WriteDisposition disposition, R value) {}
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:jpa-postgresql:contractTest --tests 'io.backend.skeleton.jpa.postgresql.write.PostgreSqlUpsertContractTest'
./gradlew :modules:jpa:jpa-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/write/NativeWriteName.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/write/UpsertConflictTarget.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/write/PostgreSqlUpsertExecutor.java' 'modules/jpa/jpa-postgresql/src/main/java/io/backend/skeleton/jpa/postgresql/write/RegisteredPostgreSqlUpsertExecutor.java' 'modules/jpa/jpa-postgresql/src/contractTest/java/io/backend/skeleton/jpa/postgresql/write/PostgreSqlUpsertContractTest.java'
git commit -m "feat: add registered postgresql upsert writes"
Task 40: PostgreSQL COPY Bulk Loader J4 Extension 구현
Files:
- Create:
modules/jpa/jpa-postgresql-copy/src/main/java/io/backend/skeleton/jpa/postgresql/copy/CopyOperationName.java - Create:
modules/jpa/jpa-postgresql-copy/src/main/java/io/backend/skeleton/jpa/postgresql/copy/PostgreSqlCopyLoader.java - Create:
modules/jpa/jpa-postgresql-copy/src/main/java/io/backend/skeleton/jpa/postgresql/copy/CopyFormat.java - Create:
modules/jpa/jpa-postgresql-copy/src/main/java/io/backend/skeleton/jpa/postgresql/copy/CopyResult.java - Test:
modules/jpa/jpa-postgresql-copy/src/integrationTest/java/io/backend/skeleton/jpa/postgresql/copy/PostgreSqlCopyLoaderIntegrationTest.java
Interfaces:
- Consumes: PostgreSQL JDBC
CopyManager, admin capability token and bounded input stream. - Produces: Explicit J4 bulk load with row/byte limits, transaction policy and audit identity.
Implementation requirements:
-
Require a registered COPY statement; no caller-provided table or column strings.
-
Enforce max rows, max bytes and finite timeout.
-
Run only under a configured bulk/admin role.
-
Return rows and bytes; never use Entity callbacks or Persistence Context.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.postgresql.copy;
class PostgreSqlCopyLoaderIntegrationTest {
@Test
void loadsBoundedCsvWithoutEntityHydration() {
var result = loader.load(IMPORT, csvOf(10_000), limits(10_000, 5_000_000));
assertThat(result.rows()).isEqualTo(10_000);
assertThat(hibernateStatistics.entityLoadCount()).isZero();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-postgresql-copy:integrationTest --tests 'io.backend.skeleton.jpa.postgresql.copy.PostgreSqlCopyLoaderIntegrationTest'
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.postgresql.copy;
public interface PostgreSqlCopyLoader {
CopyResult load(
CopyOperationName operation,
InputStream source,
CopyLimits limits);
}
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:jpa-postgresql-copy:integrationTest --tests 'io.backend.skeleton.jpa.postgresql.copy.PostgreSqlCopyLoaderIntegrationTest'
./gradlew :modules:jpa:jpa-postgresql-copy:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-postgresql-copy/src/main/java/io/backend/skeleton/jpa/postgresql/copy/CopyOperationName.java' 'modules/jpa/jpa-postgresql-copy/src/main/java/io/backend/skeleton/jpa/postgresql/copy/PostgreSqlCopyLoader.java' 'modules/jpa/jpa-postgresql-copy/src/main/java/io/backend/skeleton/jpa/postgresql/copy/CopyFormat.java' 'modules/jpa/jpa-postgresql-copy/src/main/java/io/backend/skeleton/jpa/postgresql/copy/CopyResult.java' 'modules/jpa/jpa-postgresql-copy/src/integrationTest/java/io/backend/skeleton/jpa/postgresql/copy/PostgreSqlCopyLoaderIntegrationTest.java'
git commit -m "feat: add guarded postgresql copy bulk loader"
Task 41: Flyway Schema Policy와 Hibernate Validate Gate 구현
Files:
- Create:
modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/SchemaManagementMode.java - Create:
modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/FlywaySchemaPolicy.java - Create:
modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/FlywayValidationGate.java - Create:
modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/SchemaVersionSnapshot.java - Test:
modules/jpa/jpa-migration-flyway/src/test/java/io/backend/skeleton/jpa/migration/FlywayValidationGateTest.java
Interfaces:
- Consumes: Flyway validate/migrate information and environment profile.
- Produces: Environment-specific migration policy that never auto-repairs or allows runtime DDL mutation.
Implementation requirements:
-
Local/test/dev may migrate with migration credential; staging/prod support deployment-owned migration.
-
Hibernate validate must run after migration in tests and runtime startup.
-
Checksum mismatch, missing migration and schema mismatch fail closed.
-
Repair is represented only as an admin operation descriptor, not startup behavior.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.migration;
class FlywayValidationGateTest {
@Test
void checksumMismatchFailsAndNeverRepairsAutomatically() {
var result = validationResultWithChecksumMismatch();
assertThatThrownBy(() -> gate.requireValid(result))
.isInstanceOf(SchemaMismatchException.class);
assertThat(flywayProbe.repairInvocations()).isZero();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-migration-flyway:test --tests 'io.backend.skeleton.jpa.migration.FlywayValidationGateTest'
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.migration;
public final class FlywayValidationGate {
public void requireValid(ValidateResult result) {
if (!result.validationSuccessful) {
throw new SchemaMismatchException(
"Flyway validation failed: " + sanitizedErrorCodes(result));
}
}
}
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:jpa-migration-flyway:test --tests 'io.backend.skeleton.jpa.migration.FlywayValidationGateTest'
./gradlew :modules:jpa:jpa-migration-flyway:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/SchemaManagementMode.java' 'modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/FlywaySchemaPolicy.java' 'modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/FlywayValidationGate.java' 'modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/SchemaVersionSnapshot.java' 'modules/jpa/jpa-migration-flyway/src/test/java/io/backend/skeleton/jpa/migration/FlywayValidationGateTest.java'
git commit -m "feat: enforce flyway schema validation policy"
Task 42: Migration Snapshot Upgrade Testkit 구현
Files:
- Create:
modules/jpa/jpa-testkit-migration/src/main/java/io/backend/skeleton/jpa/testkit/migration/MigrationSnapshot.java - Create:
modules/jpa/jpa-testkit-migration/src/main/java/io/backend/skeleton/jpa/testkit/migration/MigrationScenario.java - Create:
modules/jpa/jpa-testkit-migration/src/main/java/io/backend/skeleton/jpa/testkit/migration/MigrationContractRunner.java - Create:
modules/jpa/jpa-testkit-migration/src/migrationTest/java/io/backend/skeleton/jpa/testkit/migration/FlywayUpgradeContractTest.java - Test:
modules/jpa/jpa-testkit-migration/src/test/java/io/backend/skeleton/jpa/testkit/migration/MigrationScenarioTest.java
Interfaces:
- Consumes: PostgreSQL containers, schema snapshots and Task 41 validation gate.
- Produces: Repeatable empty, N-1 and oldest-supported upgrade scenarios plus checksum/missing migration failures.
Implementation requirements:
-
Restore snapshots into a clean database before each scenario.
-
Run migrations and Hibernate validate after upgrade.
-
Assert data invariants as well as schema version.
-
Persist recovery instructions for non-transactional migration failures.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.testkit.migration;
class MigrationScenarioTest {
@Test
void requiresEmptyPreviousAndOldestSupportedScenarios() {
assertThat(MigrationScenario.required())
.extracting(MigrationScenario::name)
.containsExactlyInAnyOrder("empty", "previous-release", "oldest-supported");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-testkit-migration:test --tests 'io.backend.skeleton.jpa.testkit.migration.MigrationScenarioTest'
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.testkit.migration;
public record MigrationScenario(
String name,
MigrationSnapshot snapshot,
Consumer<DataSource> invariant) {
public static List<MigrationScenario> required() {
return List.of(empty(), previousRelease(), oldestSupported());
}
}
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:jpa-testkit-migration:test --tests 'io.backend.skeleton.jpa.testkit.migration.MigrationScenarioTest'
./gradlew :modules:jpa:jpa-testkit-migration:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-testkit-migration/src/main/java/io/backend/skeleton/jpa/testkit/migration/MigrationSnapshot.java' 'modules/jpa/jpa-testkit-migration/src/main/java/io/backend/skeleton/jpa/testkit/migration/MigrationScenario.java' 'modules/jpa/jpa-testkit-migration/src/main/java/io/backend/skeleton/jpa/testkit/migration/MigrationContractRunner.java' 'modules/jpa/jpa-testkit-migration/src/migrationTest/java/io/backend/skeleton/jpa/testkit/migration/FlywayUpgradeContractTest.java' 'modules/jpa/jpa-testkit-migration/src/test/java/io/backend/skeleton/jpa/testkit/migration/MigrationScenarioTest.java'
git commit -m "test: add flyway upgrade snapshot contracts"
Task 43: Non-transactional Concurrent Index Migration Guard 구현
Files:
- Create:
modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/NonTransactionalMigrationPolicy.java - Create:
modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/ConcurrentIndexMigrationInspector.java - Create:
modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/FailedConcurrentIndexRecovery.java - Test:
modules/jpa/jpa-migration-flyway/src/test/java/io/backend/skeleton/jpa/migration/ConcurrentIndexMigrationInspectorTest.java
Interfaces:
- Consumes: Flyway migration resource metadata and PostgreSQL index catalog.
- Produces: A gate ensuring
CREATE INDEX CONCURRENTLYis explicitly non-transactional and recoverable.
Implementation requirements:
-
Detect concurrent index SQL in transactional migrations and fail validation.
-
Require a companion
.confor registered policy marking execute-in-transaction false. -
Detect invalid indexes after failed migration and generate a bounded recovery report.
-
Do not auto-drop invalid indexes in application startup.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.migration;
class ConcurrentIndexMigrationInspectorTest {
@Test
void concurrentIndexMustBeMarkedNonTransactional() {
var migration = sql("V42__order_index.sql", "create index concurrently ix_order on orders(created_at)");
assertThatThrownBy(() -> inspector.validate(migration, transactionEnabled()))
.hasMessageContaining("executeInTransaction=false");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-migration-flyway:test --tests 'io.backend.skeleton.jpa.migration.ConcurrentIndexMigrationInspectorTest'
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.migration;
public final class ConcurrentIndexMigrationInspector {
public void validate(MigrationResource migration, boolean executeInTransaction) {
if (migration.sql().toLowerCase(Locale.ROOT).contains("create index concurrently") &&
executeInTransaction) {
throw new IllegalStateException(
migration.name() + " must set executeInTransaction=false");
}
}
}
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:jpa-migration-flyway:test --tests 'io.backend.skeleton.jpa.migration.ConcurrentIndexMigrationInspectorTest'
./gradlew :modules:jpa:jpa-migration-flyway:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/NonTransactionalMigrationPolicy.java' 'modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/ConcurrentIndexMigrationInspector.java' 'modules/jpa/jpa-migration-flyway/src/main/java/io/backend/skeleton/jpa/migration/FailedConcurrentIndexRecovery.java' 'modules/jpa/jpa-migration-flyway/src/test/java/io/backend/skeleton/jpa/migration/ConcurrentIndexMigrationInspectorTest.java'
git commit -m "feat: guard concurrent index migrations"
Task 44: PostgreSQL Query Plan Testkit 구현
Files:
- Create:
modules/jpa/jpa-testkit-queryplan/src/main/java/io/backend/skeleton/jpa/testkit/queryplan/QueryPlanExpectation.java - Create:
modules/jpa/jpa-testkit-queryplan/src/main/java/io/backend/skeleton/jpa/testkit/queryplan/PostgreSqlExplainRunner.java - Create:
modules/jpa/jpa-testkit-queryplan/src/main/java/io/backend/skeleton/jpa/testkit/queryplan/NormalizedPlan.java - Create:
modules/jpa/jpa-testkit-queryplan/src/main/java/io/backend/skeleton/jpa/testkit/queryplan/QueryPlanAssertions.java - Test:
modules/jpa/jpa-testkit-queryplan/src/test/java/io/backend/skeleton/jpa/testkit/queryplan/QueryPlanAssertionsTest.java
Interfaces:
- Consumes: Registered SQL/parameters under a test/admin role and
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON). - Produces: Structural plan assertions for node types, row-estimate ratio, sort spill and buffer use.
Implementation requirements:
-
Do not globally fail every sequential scan.
-
Normalize volatile cost/time fields before snapshot comparison.
-
Require representative parameters and fixture statistics.
-
Never run ANALYZE write queries outside isolated test databases.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.testkit.queryplan;
class QueryPlanAssertionsTest {
@Test
void detectsUnexpectedSortSpillAndEstimateError() {
var plan = planWithDiskSortAndEstimateRatio(100.0);
assertThatThrownBy(() -> assertions.assertMatches(plan,
expectation().maxEstimateRatio(10).forbidDiskSort()))
.hasMessageContaining("Disk Sort");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-testkit-queryplan:test --tests 'io.backend.skeleton.jpa.testkit.queryplan.QueryPlanAssertionsTest'
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.testkit.queryplan;
public record QueryPlanExpectation(
Set<String> requiredNodeTypes,
Set<String> forbiddenNodeTypes,
double maxEstimateRatio,
boolean forbidDiskSort) {
}
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:jpa-testkit-queryplan:test --tests 'io.backend.skeleton.jpa.testkit.queryplan.QueryPlanAssertionsTest'
./gradlew :modules:jpa:jpa-testkit-queryplan:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-testkit-queryplan/src/main/java/io/backend/skeleton/jpa/testkit/queryplan/QueryPlanExpectation.java' 'modules/jpa/jpa-testkit-queryplan/src/main/java/io/backend/skeleton/jpa/testkit/queryplan/PostgreSqlExplainRunner.java' 'modules/jpa/jpa-testkit-queryplan/src/main/java/io/backend/skeleton/jpa/testkit/queryplan/NormalizedPlan.java' 'modules/jpa/jpa-testkit-queryplan/src/main/java/io/backend/skeleton/jpa/testkit/queryplan/QueryPlanAssertions.java' 'modules/jpa/jpa-testkit-queryplan/src/test/java/io/backend/skeleton/jpa/testkit/queryplan/QueryPlanAssertionsTest.java'
git commit -m "test: add postgresql query plan regression toolkit"
Task 45: Database Role·search_path Security Verifier 구현
Files:
- Create:
modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/DatabaseRolePolicy.java - Create:
modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/PostgreSqlRuntimeRoleVerifier.java - Create:
modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/SearchPathPolicy.java - Create:
modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/DatabasePrivilegeReport.java - Test:
modules/jpa/jpa-security/src/integrationTest/java/io/backend/skeleton/jpa/security/PostgreSqlRuntimeRoleVerifierIntegrationTest.java
Interfaces:
- Consumes: Runtime DataSource,
current_user,current_setting(search_path)and privilege functions. - Produces: Fail-fast proof that runtime role has DML but lacks DDL and untrusted schema CREATE privilege.
Implementation requirements:
-
Verify current user and schema against configured allowlists.
-
Reject runtime role with CREATE on application schema or database.
-
Reject untrusted writable schemas in search_path.
-
Do not expose usernames or JDBC URLs in Actuator output beyond bounded profile names.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.security;
class PostgreSqlRuntimeRoleVerifierIntegrationTest {
@Test
void runtimeRoleCanWriteRowsButCannotCreateTable() {
verifier.requireSafe(runtimeDataSource, policy());
assertThatThrownBy(() -> jdbc.execute("create table forbidden(id bigint)"))
.isInstanceOf(DataAccessException.class);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-security:integrationTest --tests 'io.backend.skeleton.jpa.security.PostgreSqlRuntimeRoleVerifierIntegrationTest'
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.security;
public final class PostgreSqlRuntimeRoleVerifier {
public DatabasePrivilegeReport verify(DataSource dataSource, DatabaseRolePolicy policy) {
return jdbc(dataSource).queryForObject("""
select current_user,
current_setting('search_path'),
has_schema_privilege(current_user, current_schema(), 'CREATE')
""", reportMapper);
}
public void requireSafe(DataSource dataSource, DatabaseRolePolicy policy) {
DatabasePrivilegeReport report = verify(dataSource, policy);
policy.requireSafe(report);
}
}
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:jpa-security:integrationTest --tests 'io.backend.skeleton.jpa.security.PostgreSqlRuntimeRoleVerifierIntegrationTest'
./gradlew :modules:jpa:jpa-security:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/DatabaseRolePolicy.java' 'modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/PostgreSqlRuntimeRoleVerifier.java' 'modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/SearchPathPolicy.java' 'modules/jpa/jpa-security/src/main/java/io/backend/skeleton/jpa/security/DatabasePrivilegeReport.java' 'modules/jpa/jpa-security/src/integrationTest/java/io/backend/skeleton/jpa/security/PostgreSqlRuntimeRoleVerifierIntegrationTest.java'
git commit -m "feat: verify postgresql runtime role safety"
Task 46: Hibernate Second-level Cache Opt-in 모듈 구현
Files:
- Create:
modules/jpa/jpa-cache-hibernate/src/main/java/io/backend/skeleton/jpa/cache/HibernateCachePolicy.java - Create:
modules/jpa/jpa-cache-hibernate/src/main/java/io/backend/skeleton/jpa/cache/CacheRegionCatalog.java - Create:
modules/jpa/jpa-cache-hibernate/src/main/java/io/backend/skeleton/jpa/cache/HibernateCacheGuard.java - Test:
modules/jpa/jpa-cache-hibernate/src/test/java/io/backend/skeleton/jpa/cache/HibernateCacheGuardTest.java
Interfaces:
- Consumes: Hibernate L2 cache settings and Entity metadata.
- Produces: ENABLE_SELECTIVE, Entity-by-Entity cache enrollment while keeping Query Cache disabled by default.
Implementation requirements:
-
Fail if Query Cache is enabled without an explicit experimental approval.
-
Require registered cache region and concurrency strategy for each cached Entity.
-
Require a Bulk DML eviction strategy.
-
Document external DB writer and cluster invalidation assumptions.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.cache;
class HibernateCacheGuardTest {
@Test
void queryCacheIsOffAndOnlyRegisteredEntitiesAreCacheable() {
assertThatThrownBy(() -> guard.validate(settings(queryCacheEnabled()), catalog()))
.hasMessageContaining("Query Cache is disabled by default");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-cache-hibernate:test --tests 'io.backend.skeleton.jpa.cache.HibernateCacheGuardTest'
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.cache;
public final class HibernateCacheGuard {
public void validate(HibernateCacheSettings settings, CacheRegionCatalog catalog) {
if (settings.queryCacheEnabled()) {
throw new IllegalStateException("Query Cache is disabled by default");
}
if (settings.sharedCacheMode() != SharedCacheMode.ENABLE_SELECTIVE) {
throw new IllegalStateException("Use ENABLE_SELECTIVE for L2 cache");
}
catalog.validate(settings.cacheableEntities());
}
}
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:jpa-cache-hibernate:test --tests 'io.backend.skeleton.jpa.cache.HibernateCacheGuardTest'
./gradlew :modules:jpa:jpa-cache-hibernate:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-cache-hibernate/src/main/java/io/backend/skeleton/jpa/cache/HibernateCachePolicy.java' 'modules/jpa/jpa-cache-hibernate/src/main/java/io/backend/skeleton/jpa/cache/CacheRegionCatalog.java' 'modules/jpa/jpa-cache-hibernate/src/main/java/io/backend/skeleton/jpa/cache/HibernateCacheGuard.java' 'modules/jpa/jpa-cache-hibernate/src/test/java/io/backend/skeleton/jpa/cache/HibernateCacheGuardTest.java'
git commit -m "feat: add opt in hibernate second level cache guard"
Task 47: Hibernate Envers Entity History Opt-in 모듈 구현
Files:
- Create:
modules/jpa/jpa-envers/src/main/java/io/backend/skeleton/jpa/envers/EnversHistoryPolicy.java - Create:
modules/jpa/jpa-envers/src/main/java/io/backend/skeleton/jpa/envers/EnversRevisionMetadata.java - Create:
modules/jpa/jpa-envers/src/main/java/io/backend/skeleton/jpa/envers/EnversHistoryReader.java - Create:
modules/jpa/jpa-envers/src/main/java/io/backend/skeleton/jpa/envers/EnversConfigurationGuard.java - Test:
modules/jpa/jpa-envers/src/integrationTest/java/io/backend/skeleton/jpa/envers/EnversHistoryContractTest.java
Interfaces:
- Consumes: Hibernate Envers and application-provided revision actor/context.
- Produces: Entity-specific history without conflating it with technical or business audit.
Implementation requirements:
-
Require explicit
@Auditedor catalog enrollment. -
Record bounded actor/correlation metadata, not entire security principals.
-
Require retention and PII deletion policy before production enablement.
-
Do not enable Envers for every Entity through a global base class.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.envers;
class EnversHistoryContractTest {
@Test
void storesHistoryOnlyForOptedInEntity() {
updateAuditedEntity();
updateNonAuditedEntity();
assertThat(reader.revisions(AuditedFixture.class, auditedId)).hasSize(2);
assertThat(reader.revisions(PlainFixture.class, plainId)).isEmpty();
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-envers:integrationTest --tests 'io.backend.skeleton.jpa.envers.EnversHistoryContractTest'
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.envers;
public interface EnversHistoryReader {
<T> List<EntityRevision<T>> revisions(Class<T> entityType, Object id);
}
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:jpa-envers:integrationTest --tests 'io.backend.skeleton.jpa.envers.EnversHistoryContractTest'
./gradlew :modules:jpa:jpa-envers:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-envers/src/main/java/io/backend/skeleton/jpa/envers/EnversHistoryPolicy.java' 'modules/jpa/jpa-envers/src/main/java/io/backend/skeleton/jpa/envers/EnversRevisionMetadata.java' 'modules/jpa/jpa-envers/src/main/java/io/backend/skeleton/jpa/envers/EnversHistoryReader.java' 'modules/jpa/jpa-envers/src/main/java/io/backend/skeleton/jpa/envers/EnversConfigurationGuard.java' 'modules/jpa/jpa-envers/src/integrationTest/java/io/backend/skeleton/jpa/envers/EnversHistoryContractTest.java'
git commit -m "feat: add opt in hibernate envers history"
Task 48: JPA Metrics·Tracing·Log Redaction 구현
Files:
- Create:
modules/jpa/jpa-observability/src/main/java/io/backend/skeleton/jpa/observation/MicrometerQueryObservation.java - Create:
modules/jpa/jpa-observability/src/main/java/io/backend/skeleton/jpa/observation/JpaTransactionObservation.java - Create:
modules/jpa/jpa-observability/src/main/java/io/backend/skeleton/jpa/observation/JpaRetryObservation.java - Create:
modules/jpa/jpa-observability/src/main/java/io/backend/skeleton/jpa/observation/JpaMetricTags.java - Create:
modules/jpa/jpa-observability/src/main/java/io/backend/skeleton/jpa/observation/SqlDiagnosticRedactor.java - Test:
modules/jpa/jpa-observability/src/test/java/io/backend/skeleton/jpa/observation/JpaObservabilityContractTest.java
Interfaces:
- Consumes: Micrometer, Spring Observation, QueryName, PersistenceOperationName and Hibernate statistics.
- Produces: Logical transaction/query/retry metrics with bounded tags and PII-safe diagnostics.
Implementation requirements:
-
Measure transaction count/duration/rollback/timeout/retry/completion-unknown.
-
Measure query count/duration/rows/fetch metrics and JDBC batch count.
-
Allow only registered operation/query/entity type tags.
-
Reject SQL parameters, IDs, tenant values and dynamic exception messages from metric tags.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.observation;
class JpaObservabilityContractTest {
@Test
void metricsNeverUseEntityIdOrSqlParameterAsTag() {
observation.recordFailure(OPERATION, QUERY, uniqueViolation("secret@example.test"));
assertThat(registry.getMeters())
.flatExtracting(meter -> meter.getId().getTags())
.extracting(Tag::getValue)
.noneMatch(value -> value.contains("secret@example.test") || value.contains("entity-42"));
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-observability:test --tests 'io.backend.skeleton.jpa.observation.JpaObservabilityContractTest'
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.observation;
public record JpaMetricTags(
String persistenceUnit,
String operationName,
String queryName,
String outcome,
String failureCategory) {
public JpaMetricTags {
LowCardinality.requireRegistered(operationName, queryName, failureCategory);
}
}
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:jpa-observability:test --tests 'io.backend.skeleton.jpa.observation.JpaObservabilityContractTest'
./gradlew :modules:jpa:jpa-observability:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-observability/src/main/java/io/backend/skeleton/jpa/observation/MicrometerQueryObservation.java' 'modules/jpa/jpa-observability/src/main/java/io/backend/skeleton/jpa/observation/JpaTransactionObservation.java' 'modules/jpa/jpa-observability/src/main/java/io/backend/skeleton/jpa/observation/JpaRetryObservation.java' 'modules/jpa/jpa-observability/src/main/java/io/backend/skeleton/jpa/observation/JpaMetricTags.java' 'modules/jpa/jpa-observability/src/main/java/io/backend/skeleton/jpa/observation/SqlDiagnosticRedactor.java' 'modules/jpa/jpa-observability/src/test/java/io/backend/skeleton/jpa/observation/JpaObservabilityContractTest.java'
git commit -m "feat: add safe jpa observability contracts"
Task 49: PostgreSQL 16·17·18 공통 Contract Suite 구현
Files:
- Create:
modules/jpa/jpa-testkit-postgresql/src/main/java/io/backend/skeleton/jpa/testkit/postgresql/PostgreSqlVersion.java - Create:
modules/jpa/jpa-testkit-postgresql/src/main/java/io/backend/skeleton/jpa/testkit/postgresql/PostgreSqlContainerFactory.java - Create:
modules/jpa/jpa-testkit-postgresql/src/main/java/io/backend/skeleton/jpa/testkit/postgresql/PostgreSqlContractExtension.java - Create:
modules/jpa/jpa-testkit-postgresql/src/contractTest/java/io/backend/skeleton/jpa/testkit/postgresql/StablePostgreSqlMatrixContractTest.java - Test:
modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/postgresql/PostgreSqlVersionTest.java
Interfaces:
- Consumes: All Stable mapping, transaction, query, fetch, batch, extension and security contracts.
- Produces: A parameterized release matrix over real PostgreSQL 16, 17 and 18 containers.
Implementation requirements:
-
PR profile runs 16 and 18; release profile runs 16, 17 and 18.
-
Pin image digests or approved tags and record exact server version.
-
Run Flyway before Hibernate validate.
-
H2 results must not satisfy this suite.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.testkit.postgresql;
class PostgreSqlVersionTest {
@Test
void stableVersionsAreExactlySixteenSeventeenAndEighteen() {
assertThat(PostgreSqlVersion.stable())
.containsExactly(PG_16, PG_17, PG_18);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.postgresql.PostgreSqlVersionTest'
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.testkit.postgresql;
public enum PostgreSqlVersion {
PG_16("postgres:16"),
PG_17("postgres:17"),
PG_18("postgres:18");
public static List<PostgreSqlVersion> stable() {
return List.of(PG_16, PG_17, PG_18);
}
}
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:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.postgresql.PostgreSqlVersionTest'
./gradlew :modules:jpa:jpa-testkit-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-testkit-postgresql/src/main/java/io/backend/skeleton/jpa/testkit/postgresql/PostgreSqlVersion.java' 'modules/jpa/jpa-testkit-postgresql/src/main/java/io/backend/skeleton/jpa/testkit/postgresql/PostgreSqlContainerFactory.java' 'modules/jpa/jpa-testkit-postgresql/src/main/java/io/backend/skeleton/jpa/testkit/postgresql/PostgreSqlContractExtension.java' 'modules/jpa/jpa-testkit-postgresql/src/contractTest/java/io/backend/skeleton/jpa/testkit/postgresql/StablePostgreSqlMatrixContractTest.java' 'modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/postgresql/PostgreSqlVersionTest.java'
git commit -m "test: add postgresql stable compatibility matrix"
Task 50: Deadlock·Serialization·Commit Ambiguity Failure Injection Suite 구현
Files:
- Create:
modules/jpa/jpa-testkit-postgresql/src/main/java/io/backend/skeleton/jpa/testkit/failure/PostgreSqlFailureScenario.java - Create:
modules/jpa/jpa-testkit-postgresql/src/main/java/io/backend/skeleton/jpa/testkit/failure/CommitAmbiguityProxy.java - Create:
modules/jpa/jpa-testkit-postgresql/src/failureTest/java/io/backend/skeleton/jpa/testkit/failure/PostgreSqlConcurrencyFailureContractTest.java - Create:
modules/jpa/jpa-testkit-postgresql/src/failureTest/java/io/backend/skeleton/jpa/testkit/failure/CommitAmbiguityContractTest.java - Modify:
infra/jpa/toxiproxy/docker-compose.yml - Test:
modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/failure/PostgreSqlFailureScenarioTest.java
Interfaces:
- Consumes: Toxiproxy, deterministic transaction barriers, Task 6 evidence manager and Task 8 retry coordinator.
- Produces: Reproducible
40P01,40001and commit-response-loss scenarios.
Implementation requirements:
-
Deadlock uses opposite lock order and confirms bounded full-TX retry.
-
Serialization uses SERIALIZABLE invariant contention.
-
Commit ambiguity distinguishes before-COMMIT, during-COMMIT and after-server-commit response loss.
-
After-server-commit loss must emit completion unknown and must not rerun the original mutation.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.testkit.failure;
class PostgreSqlFailureScenarioTest {
@Test
void commitAmbiguityHasThreeDistinctInjectionPoints() {
assertThat(PostgreSqlFailureScenario.commitPoints())
.containsExactly(BEFORE_COMMIT, DURING_COMMIT, AFTER_SERVER_COMMIT_BEFORE_RESPONSE);
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.failure.PostgreSqlFailureScenarioTest'
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.testkit.failure;
public enum PostgreSqlFailureScenario {
BEFORE_COMMIT,
DURING_COMMIT,
AFTER_SERVER_COMMIT_BEFORE_RESPONSE;
public static List<PostgreSqlFailureScenario> commitPoints() {
return List.of(values());
}
}
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:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.failure.PostgreSqlFailureScenarioTest'
./gradlew :modules:jpa:jpa-testkit-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-testkit-postgresql/src/main/java/io/backend/skeleton/jpa/testkit/failure/PostgreSqlFailureScenario.java' 'modules/jpa/jpa-testkit-postgresql/src/main/java/io/backend/skeleton/jpa/testkit/failure/CommitAmbiguityProxy.java' 'modules/jpa/jpa-testkit-postgresql/src/failureTest/java/io/backend/skeleton/jpa/testkit/failure/PostgreSqlConcurrencyFailureContractTest.java' 'modules/jpa/jpa-testkit-postgresql/src/failureTest/java/io/backend/skeleton/jpa/testkit/failure/CommitAmbiguityContractTest.java' 'infra/jpa/toxiproxy/docker-compose.yml' 'modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/failure/PostgreSqlFailureScenarioTest.java'
git commit -m "test: add jpa concurrency and commit ambiguity failures"
Task 51: Hikari Pool·REQUIRES_NEW Saturation Contract 구현
Files:
- Create:
modules/jpa/jpa-testkit-postgresql/src/performanceTest/java/io/backend/skeleton/jpa/testkit/pool/HikariPoolSaturationContractTest.java - Create:
modules/jpa/jpa-testkit-postgresql/src/performanceTest/java/io/backend/skeleton/jpa/testkit/pool/RequiresNewPoolPressureContractTest.java - Create:
modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/pool/PoolMeasurement.java - Test:
modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/pool/PoolMeasurementTest.java
Interfaces:
- Consumes: Hikari metrics, bounded executor and nested transaction fixtures.
- Produces: Evidence for pending/acquire latency, connection timeout and outer+inner connection pressure.
Implementation requirements:
-
Test finite pool saturation without changing production defaults.
-
Show that concurrent REQUIRED uses one connection per transaction while REQUIRES_NEW can require two.
-
Ensure rejected/acquire-timeout work releases all connections.
-
Record transaction duration and pending acquire latency together.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.testkit.pool;
class PoolMeasurementTest {
@Test
void reportsPendingAndAcquireLatencyTogether() {
var measurement = new PoolMeasurement(4, 2, 3, Duration.ofMillis(80));
assertThat(measurement.pending()).isEqualTo(3);
assertThat(measurement.acquireLatency()).isEqualTo(Duration.ofMillis(80));
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.pool.PoolMeasurementTest'
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.testkit.pool;
public record PoolMeasurement(
int active,
int idle,
int pending,
Duration acquireLatency) {
}
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:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.pool.PoolMeasurementTest'
./gradlew :modules:jpa:jpa-testkit-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-testkit-postgresql/src/performanceTest/java/io/backend/skeleton/jpa/testkit/pool/HikariPoolSaturationContractTest.java' 'modules/jpa/jpa-testkit-postgresql/src/performanceTest/java/io/backend/skeleton/jpa/testkit/pool/RequiresNewPoolPressureContractTest.java' 'modules/jpa/jpa-testkit-postgresql/src/testFixtures/java/io/backend/skeleton/jpa/testkit/pool/PoolMeasurement.java' 'modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/pool/PoolMeasurementTest.java'
git commit -m "test: certify hikari and requires new pool behavior"
Task 52: Spring Boot Starter·Actuator·Capability Report 완성
Files:
- Create:
modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaPlatformAutoConfiguration.java - Create:
modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaTransactionAutoConfiguration.java - Create:
modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaObservabilityAutoConfiguration.java - Create:
modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaPlatformEndpoint.java - Create:
modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaPlatformReport.java - Modify:
modules/jpa/jpa-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - Test:
modules/jpa/jpa-spring-boot-starter/src/test/java/io/backend/skeleton/jpa/autoconfigure/JpaPlatformAutoConfigurationTest.java
Interfaces:
- Consumes: Tasks 6
12, 18, 2225, 41, 45 and 48. - Produces: Conditional Stable auto-configuration and a sanitized actuator endpoint.
Implementation requirements:
-
Back off when the application supplies its own transaction manager or observation implementation.
-
Auto-configure only Stable modules; Querydsl, Envers, L2 and COPY require explicit dependencies/properties.
-
Endpoint reports DB major version, provider version, schema version, OSIV, role verification and capabilities.
-
Do not expose JDBC URL, username, SQL, credentials or Entity catalog.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.autoconfigure;
class JpaPlatformAutoConfigurationTest {
@Test
void configuresStablePlatformAndSanitizesEndpoint() {
context.withUserConfiguration(TestJpaApplication.class)
.run(result -> {
assertThat(result).hasSingleBean(JpaTransactionExecutor.class);
assertThat(result.getBean(JpaPlatformEndpoint.class).platform())
.doesNotHaveToString(".*jdbc:.*|.*password.*");
});
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-spring-boot-starter:test --tests 'io.backend.skeleton.jpa.autoconfigure.JpaPlatformAutoConfigurationTest'
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.autoconfigure;
@AutoConfiguration
@EnableConfigurationProperties({JpaSafetyProperties.class, JpaDataSourceProperties.class})
public class JpaPlatformAutoConfiguration {
@Bean
JpaPlatformReport jpaPlatformReport(
DatabaseMetadata metadata,
FlywaySchemaPolicy schema,
DatabasePrivilegeReport privileges) {
return JpaPlatformReport.sanitized(metadata, schema, privileges);
}
}
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:jpa-spring-boot-starter:test --tests 'io.backend.skeleton.jpa.autoconfigure.JpaPlatformAutoConfigurationTest'
./gradlew :modules:jpa:jpa-spring-boot-starter:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add 'modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaPlatformAutoConfiguration.java' 'modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaTransactionAutoConfiguration.java' 'modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaObservabilityAutoConfiguration.java' 'modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaPlatformEndpoint.java' 'modules/jpa/jpa-spring-boot-starter/src/main/java/io/backend/skeleton/jpa/autoconfigure/JpaPlatformReport.java' 'modules/jpa/jpa-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports' 'modules/jpa/jpa-spring-boot-starter/src/test/java/io/backend/skeleton/jpa/autoconfigure/JpaPlatformAutoConfigurationTest.java'
git commit -m "feat: complete jpa spring boot starter and actuator"
Task 53: CI Matrix·문서·ADR·Release Gate 완성
Files:
- Create:
.github/workflows/jpa-pr.yml - Create:
.github/workflows/jpa-nightly.yml - Create:
.github/workflows/jpa-release.yml - Create:
docs/jpa/support-matrix.md - Create:
docs/jpa/entity-mapping-guide.md - Create:
docs/jpa/transaction-guide.md - Create:
docs/jpa/query-fetch-guide.md - Create:
docs/jpa/migration-guide.md - Create:
docs/jpa/postgresql-extensions.md - Create:
docs/jpa/observability.md - Create:
docs/jpa/security.md - Create:
docs/jpa/runbooks.md - Create:
docs/adr/ADR-JPA-001-domain-owns-persistence-model.md - Create:
docs/adr/ADR-JPA-002-full-transaction-retry.md - Create:
docs/adr/ADR-JPA-003-completion-unknown.md - Create:
docs/adr/ADR-JPA-004-flyway-schema-source-of-truth.md - Create:
docs/adr/ADR-JPA-005-postgresql-real-contract.md - Modify:
build.gradle.kts - Test:
modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/release/JpaReleaseManifestTest.java
Interfaces:
- Consumes: All Stable modules, test suites, design decisions and support matrix.
- Produces: PR/nightly/release aggregation, operator documentation and a machine-readable release manifest.
Implementation requirements:
-
PR runs unit, architecture, PG16·18 contract and migration smoke.
-
Nightly runs PG16·17·18, failure, plan, pool and security suites.
-
Release runs all Stable contracts, upgrade snapshots, performance and artifact compatibility checks.
-
Document Stable/Advanced/Experimental/Unsupported features exactly as the design.
-
Release fails if H2 is the only database test, OSIV is on, ddl-auto mutates schema, completion unknown retry exists or runtime DDL succeeds.
-
Step 1: Write the failing test
package io.backend.skeleton.jpa.testkit.release;
class JpaReleaseManifestTest {
@Test
void manifestContainsAllStableVersionsAndMandatoryGates() {
var manifest = JpaReleaseManifest.load("docs/jpa/support-matrix.md");
assertThat(manifest.postgreSqlVersions()).containsExactly(16, 17, 18);
assertThat(manifest.gates()).contains(
"completion-unknown-no-retry",
"osiv-disabled",
"flyway-validate",
"runtime-role-no-ddl",
"hibernate-7.4-fetch-pagination");
}
}
- Step 2: Run the focused test and verify the failure
Run:
./gradlew :modules:jpa:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.release.JpaReleaseManifestTest'
Expected: FAIL because the production type or behavior does not exist yet.
- Step 3: Implement the smallest complete production contract
plugins {
base
}
tasks.register("jpaReleaseGate") {
dependsOn(
":modules:jpa:jpa-testkit-postgresql:contractTest",
":modules:jpa:jpa-testkit-postgresql:failureTest",
":modules:jpa:jpa-testkit-postgresql:performanceTest",
":modules:jpa:jpa-testkit-migration:migrationTest",
":modules:jpa:jpa-testkit-queryplan:test"
)
}
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:jpa-testkit-postgresql:test --tests 'io.backend.skeleton.jpa.testkit.release.JpaReleaseManifestTest'
./gradlew :modules:jpa:jpa-testkit-postgresql:test
Expected: PASS with all assertions green.
- Step 5: Commit the independently reviewable change
git add '.github/workflows/jpa-pr.yml' '.github/workflows/jpa-nightly.yml' '.github/workflows/jpa-release.yml' 'docs/jpa/support-matrix.md' 'docs/jpa/entity-mapping-guide.md' 'docs/jpa/transaction-guide.md' 'docs/jpa/query-fetch-guide.md' 'docs/jpa/migration-guide.md' 'docs/jpa/postgresql-extensions.md' 'docs/jpa/observability.md' 'docs/jpa/security.md' 'docs/jpa/runbooks.md' 'docs/adr/ADR-JPA-001-domain-owns-persistence-model.md' 'docs/adr/ADR-JPA-002-full-transaction-retry.md' 'docs/adr/ADR-JPA-003-completion-unknown.md' 'docs/adr/ADR-JPA-004-flyway-schema-source-of-truth.md' 'docs/adr/ADR-JPA-005-postgresql-real-contract.md' 'build.gradle.kts' 'modules/jpa/jpa-testkit-postgresql/src/test/java/io/backend/skeleton/jpa/testkit/release/JpaReleaseManifestTest.java'
git commit -m "docs: add jpa release matrix and runbooks"
4. 최종 실행 순서와 Review Gate
Task 1~12
→ 모듈·Core·오류·Transaction·Starter Guard
Task 13~28
→ Mapping·Persistence Context·Repository·Query·Fetch·Pagination
Task 29~32
→ Optimistic/Pessimistic·Constraint
Task 33~40
→ Batch·Bulk·Hibernate·PostgreSQL Native
Task 41~45
→ Flyway·Migration·Plan·Security
Task 46~48
→ L2 Cache·Envers·Observability
Task 49~53
→ PostgreSQL Matrix·Failure·Pool·Starter·Release
각 Task 뒤에는 두 단계 review를 수행한다.
- Specification review: 설계서의 계약과 exact type/signature가 일치하는가.
- Quality review: 테스트가 failure mode를 실제로 재현하고 위험한 우회 경로를 남기지 않는가.
Stable 계획이 끝나기 전 Experimental module을 구현하지 않는다.
5. 계획 완료 기준
53개 Task가 순서대로 존재한다.
각 Task에 정확한 파일 경로와 public interface가 있다.
각 Task가 failing test와 예상 실패를 포함한다.
각 Task가 최소 구현 코드와 pass command를 포함한다.
각 Task가 독립 commit으로 종료한다.
Generic Repository 재구현 Task가 없다.
Commit Unknown 자동 Retry가 없다.
PG16·17·18 Release Matrix가 있다.
Flyway, Security, Fetch, Batch, Pool, Failure Gate가 구현 순서에 포함된다.