Files
clean-architecture-backend-…/docs/adr/ADR-JPA-005-postgresql-real-contract.md
T
DongHyeonkaandClaude Opus 5 0e61f86eb5 feat(jpa): implement the JPA relational persistence platform
Implements the Stable and Experimental JPA persistence platform designs
against real PostgreSQL, adapted to this repository's fail-closed 19-leaf
registry.

The design models the platform as 25 Gradle projects. `src/settings.gradle`
throws unless the registry holds exactly 19 leaves, so the plan's modules
become packages inside `:adapter:outbound:persistence-jpa` (starter in
`:app-bootstrap`, testkit in its own source set). The full mapping, the
renames this repository's naming gate required, and every deliberate
substitution are recorded in `docs/jpa/repository-adaptation.md`.

Seven Docker-backed lanes replace the plan's seven JVM test suites. Each
fails closed: a lane that discovers nothing, or a container that cannot
start, is an error rather than a skip.

Three defects the contracts found against a real server:

- `CommitFailureClassifier` treated only SQLSTATE 40003, class 08, and
  transport breaks as completion-unknown. A backend terminated mid-commit
  reports 57P01, and the commit record may already be in the WAL — so a
  possibly-committed transaction could be re-run. 57P01/57P02/57P03 now
  classify as completion-unknown.
- `SchemaTenantMigrationOrchestrator` recorded `MigrateResult`'s target
  version, which is empty for a tenant already current, reporting migrated
  tenants as unmigrated during a partial rollout. It now reads the applied
  version back from the tenant's schema history.
- `JpaStreamExecutor` checked only the declared return type for reactive
  publishers, and `RegisteredPostgreSqlCopyLoader` passed the COPY timeout
  to `SET`, which is parsed before parameter binding.

`JpaModuleBoundaryTest` enforces the plan's module map as package rules;
`verifyCleanArchitectureDependencies` governs edges between leaves and
cannot see these. Its first assertion is that the import is non-empty,
because every rule under it is a `noClasses()` rule and would pass
vacuously on an empty import.

Verified: 128 container tests across all seven lanes, 1183 unit tests,
`:adapter:outbound:persistence-jpa:check`, `:app-bootstrap:check`,
`verifyCleanArchitectureDependencies`, `verifyOneTypePerFile`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 14:06:18 +09:00

1.5 KiB

ADR-JPA-005 — Contracts run against real PostgreSQL

  • Status: Accepted
  • Date: 2026-08-11
  • Design: §40

Context

An in-memory database makes tests fast and hermetic. A container makes them slow and requires Docker.

Decision

Every persistence contract runs against real PostgreSQL 16, 17, and 18 in containers. H2 remains a local-development convenience and never satisfies a contract. The lanes fail closed when Docker is absent rather than skipping.

Consequences

The behaviours these contracts verify either do not exist in H2 or differ there: SQLSTATE values for the same violation, FOR UPDATE SKIP LOCKED semantics, JSONB operators, range types, concurrent index builds, search_path privileges, and the generated SQL for a paged collection fetch. A green H2 run is evidence that the code compiles and runs — not that any of the above holds.

Three versions rather than one because the platform claims three. A contract suite that ran only on 16 would make "Stable on 17 and 18" an assumption.

Skipping on missing Docker is the failure mode this decision most wants to avoid: a skipped contract reports success, and CI eventually inherits that silence. PostgreSqlContainerFactory.assertDockerAvailable() throws instead.

The cost is that the contract lanes need Docker and take minutes. The unit lane stays hermetic and fast, and is where most tests live; the container lanes verify the things only a real server can answer.

Enforcement

PostgreSqlVersion.stable(); PostgreSqlContainerFactory; release gate postgresql-contract.