Files
clean-architecture-backend-…/docs/adr/ADR-JPA-006-canonical-technical-audit.md

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.md JPA-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.