Files
clean-architecture-backend-…/docs/jpa/support-matrix.md
T

8.5 KiB

JPA Persistence Platform — Support Matrix

This document is a rendering. The machine-readable source is src/config/jpa/release-registry.json.

JpaReleaseManifest used to parse this file with regular expressions: every PostgreSQL NN it mentioned became a supported version, whatever table or sentence produced the match. An Experimental major joined the Stable list, a version named once in prose counted as supported, and demoting a major changed nothing so long as the string survived somewhere in the document. Now the registry declares a support level per major as a field, each gate names the Gradle task that produces its evidence, and this document describes what the registry says.

Being a rendering used to be a claim rather than a mechanism: the tables below were still typed by hand, so a major demoted in the registry stayed Stable here and kept its full release job. JpaReleaseRenderingTest now compares the database table, the gate table and jpa-release.yml's matrix and promotion lists to the registry, and verifyJpaReleaseGateTasks resolves every gate's task against the real Gradle task graph. Edit the registry; these tables follow, or the build fails.

Two renderings stayed outside that comparison until they were added to it. jpa-nightly.yml runs its own matrix and nothing checked it, so a demotion corrected the release lane and left the nightly lane certifying the major. And an Experimental major's "compatibility lane only" named no file: the lane existed, but the registry, this document and the release workflow could each be read end to end without establishing that, so a reader looking for it concluded there was none. An Experimental major now has to be recorded as the target of a lane in .github/workflows, and a Stable lane may not run it.

Database

Database Support Evidence
PostgreSQL 16 Stable full contract suite, release lane (own matrix job)
PostgreSQL 17 Stable full contract suite, release lane (own matrix job)
PostgreSQL 18 Stable full contract suite, release lane (own matrix job)
PostgreSQL 19 Experimental jpa-next-postgresql19.ymlNOT_EXECUTABLE: no postgres:19-alpine is published, so no container of that major has been started; promotion requires an ADR
H2 Local convenience never evidence of PostgreSQL behaviour

Each major gets its own release job, because for a while it did not. The release lane passed -Pjpa.matrix.versions=16,17,18 to a JpaPlatformContractSupport.start() that used selectedVersions().get(0), so the whole integration suite ran against PostgreSQL 16 and this table recorded 17 and 18 as fully covered on the strength of a three-assertion smoke test. start() now refuses a multi-version selection outright, jpa-release.yml fans out to one job per major, and a promotion job requires all three majors' evidence to carry the same commit SHA — so a removed major removes the release, not the evidence for it.

Provider baseline. The gates run against the Hibernate version the Spring Boot BOM resolves — 7.1.8.Final — which the registry records as stable-tested-baseline. This document previously called 7.4 the Stable baseline and the pagination gate was named hibernate-7.4-fetch-pagination, so every run of that gate produced evidence labelled with a provider it had never executed against. 7.4 is recorded as compatibility-target; it becomes the baseline when a full lane has actually run on it.

H2 is not a second production target. It reports different SQLSTATEs for the same violation, no JSONB operators, no range types, and no concurrent index builds. A green H2 run is evidence that the code compiles and runs, and nothing more.

SKIP LOCKED needs its own sentence, because two documents said different things about it. The module's CLAUDE.md records a measurement: H2 2.4.240 accepts FOR UPDATE SKIP LOCKED and does genuinely skip locked rows, which is why the outbox claim SQL is identical on both vendors. This document previously said H2 has no such guarantee. Both are right about different questions, and the distinction is the point: observed behaviour in the version we measured is not a production guarantee, and it is never PostgreSQL contract evidence. The measurement is why the claim SQL needs no vendor branch; the absence of a guarantee is why every concurrency contract still runs against a real PostgreSQL.

Specification and provider

Component Stable Experimental
Jakarta Persistence 3.2 4.0 (lane)
Hibernate ORM 7.4 declared baseline 8 (lane)
Spring Boot repository BOM

The Hibernate row needs a note. The design declares 7.4 as the Stable provider; this repository's Spring Boot BOM resolves 7.1.x. HibernateProviderPolicy holds both — the declared baseline as a constant, the resolved version read from Hibernate itself — and driftsFromDeclaredBaseline() makes the difference visible instead of asserting a constant against itself. See repository-adaptation.md §4.

Capability support levels

Capability Level
Full-transaction retry Stable
Commit completion evidence Stable
Keyset pagination Stable
JDBC batch Stable
Flyway schema gate Stable
Runtime role verification Stable
Observability Stable
PostgreSQL native write (ON CONFLICT/RETURNING) Advanced
PostgreSQL work claim (SKIP LOCKED) Advanced
PostgreSQL JSONB Advanced
PostgreSQL array and range Advanced
Bulk DML Advanced
Hibernate StatelessSession Advanced
PostgreSQL COPY Admin (J4)
Hibernate second-level cache Advanced
Hibernate Envers Advanced
Technical auditing — audit/AuditableEntity Stable (canonical)
Technical auditing — auditing/AuditMetadata Candidate, not composed
Multi-tenancy (column, RLS, schema, database) Experimental
Consistency-aware read replica Experimental

Release gates

Each row is a way the platform could pass its tests and still be wrong in production.

Gate Kind What it prevents
postgresql-contract gate a release whose only database evidence came from H2
completion-unknown-no-retry gate automatically re-running a write that may already have committed
osiv-disabled gate lazy loading from the view layer, one query per rendered row
flyway-validate gate Hibernate mutating a deployed schema, or running against one it was not built for
runtime-role-no-ddl gate the application's own credential being able to alter or drop schema objects
collection-fetch-pagination gate a paged collection fetch silently reading the whole table and paginating in memory

The two audit mechanisms

audit/AuditableEntity is the canonical one: created_*/updated_*, a 256-character actor, stamped explicitly by the repository adapter. It is what the sample entities extend and what the migrations were written for.

auditing/AuditMetadata is a second, complete mechanism with different column names (modified_*), a different actor length (64) and a different capture lifecycle (Spring Data listeners). Nothing embeds it and nothing composes JpaAuditingConfiguration, which is why it is listed as a candidate rather than as a capability: promoting it means choosing between reshaping it to the canonical columns and writing a forward migration for the new ones, and that choice has not been made. Until it is, an entity picks one mechanism or none — enforced on the production graph by JpaAuditMechanismRule.entitiesUseExactlyOneAuditMechanism.

Neither mechanism reaches a bulk or native update. Both stamp on an ordinary save — one in the adapter, one on a managed entity's lifecycle — so a statement that goes straight to the database leaves the audit columns showing the previous save. A bulk update of an audited entity must therefore set the audit column in the statement, which JpaAuditMechanismRule.bulkUpdatesOfAuditedEntitiesStampAudit checks over the production graph.

Explicitly unsupported

  • Reactive JPA. JPA is a blocking specification; a reactive facade over it moves the blocking call onto an event loop rather than removing it.
  • Hibernate as the production schema writer. ddl-auto never mutates a deployed schema.
  • A platform-owned generic CRUD repository. Domains own their repositories (design §10.1).
  • Automatic reconciliation of a completion-unknown transaction. The platform records; the domain resolves.