3.6 KiB
ADR-JPA-006 — audit is the canonical technical audit model; auditing stays a frozen candidate
- Status: Accepted
- Date: 2026-08-24
- Review:
docs/reviews/2026-08-14-jpa-module-code-review.mdJPA-022
Context
Two complete technical-audit mechanisms live in this leaf and they disagree about the schema.
audit/AuditableEntity stamps created_at/created_by/updated_at/updated_by with an actor
column of length 256, captured through explicit initializeAudit/applyModification calls and an
AuditContextPort. auditing/AuditMetadata is a Spring Data embeddable that stamps
created_*/modified_* with an actor column of length 64, captured by @CreatedDate and friends
through an AuditorAware.
Only the first is real: it is what the sample entities extend and what the migrations were written
for. JpaAuditingConfiguration is not a Spring @Configuration, and nothing in production
constructs any of the three auditing types.
The review asked for one canonical model with a migration or activation decision. The failure mode it was protecting against is specific: an author of a new entity picks whichever package they find first, and column names, actor lengths and capture lifecycles then diverge per table.
Decision
audit/AuditableEntity is canonical. auditing stays in the tree as a candidate and is excluded
from the Stable capability report.
The candidate is not deleted and not promoted. Deleting it would discard a working Spring Data
integration that a deployment preferring declarative auditing would want. Promoting it would mean
either renaming modified_* to updated_* and widening the actor column — a schema migration of
every audited table to gain nothing a caller asked for — or moving the sample entities onto
modified_*, which is the same migration in the other direction.
Neither is worth doing now. What the divergence actually needed was not consolidation but a rule that an entity cannot straddle the two, and that rule is cheaper than either migration.
Consequences
Two audit mechanisms remain readable in one leaf, and a reader has to be told which one is live. That cost is paid in this document, in the package javadoc and in a test whose name says so.
Two failure modes stay silent unless they are asserted, so both are:
- The candidate acquires a stereotype and starts stamping in every deployment that has this module
on the classpath, including the ones whose tables have no
modified_*columns — where the result is a failed startup rather than a feature. - Somebody "harmonises" the two by editing one side's column names, at which point the schema a deployed table was migrated for and the schema its entity expects diverge with no migration between them.
If the candidate is ever promoted, it is promoted atomically: forward migration, sample conversion,
AuditContextPort → AuditorAware and Clock → DateTimeProvider bridges land together, and this
ADR is superseded rather than amended.
Bulk and native updates stamp nothing under either mechanism. That is a property of JPA, not of the choice made here, so it is enforced separately rather than assumed away.
Enforcement
JpaAuditMechanismRule.entitiesUseExactlyOneAuditMechanism and
bulkUpdatesOfAuditedEntitiesStampAudit, run against the real production graph by
JpaProductionArchitectureTest at the composition root — not against fixtures, which is how the
earlier version of this rule pack passed while applying to nothing. AuditingCandidateStatusTest
asserts the candidate carries no composing stereotype and that the two column sets stay distinct.
JpaAuditMechanismRuleTest exercises the rules' own negative cases.