Javers Audit Failure: Entity vs Value Object Traps

Summary

A production audit log failure occurred where Javers failed to record granular property changes for nested objects. Instead of capturing specific field updates (e.g., product.code), the system only reported a change in the identity (ID) of the referenced entity. This resulted in a complete loss of visibility into the actual state changes of downstream domain models when updated through a parent entity.

Root Cause

The issue stems from a fundamental misunderstanding of how Javers handles the distinction between Entities and Value Objects during the diffing process:

  • Entity Identity vs. Value State: By registering Product as an Entity (via the Entity abstract class), Javers treats the object as a unique, independent identity.
  • Reference-based Diffing: When Foo contains a reference to Product, and both are registered as Entities, Javers performs a reference comparison.
  • Identity Mismatch: If the id of the Product inside Foo changes, Javers concludes that the old Product was replaced by a completely new Product entity.
  • Loss of Granularity: Because the engine sees a substitution of identity rather than a mutation of state, it stops traversing the object tree to look for property changes, reporting only the change from id: 1 to id: 3.

Why This Happens in Real Systems

In complex distributed systems, we often use a Unified Domain Model where most objects extend a base Entity class to ensure database compatibility. This leads to two systemic risks:

  • Over-registration: Developers often apply the same auditing rules to all objects to ensure “everything is covered,” inadvertently turning complex object graphs into a web of strictly-identity-bound nodes.
  • Abstraction Leaks: The technical requirement to audit an object independently (as an Entity) conflicts with the functional requirement to see its evolution when embedded within a parent (as a Value Object).

Real-World Impact

  • Audit Compliance Failure: Security audits fail because the system cannot prove what changed, only that something was replaced.
  • Debugging Blindness: When a production bug is caused by a specific field change, engineers cannot use the audit logs to reconstruct the erroneous state.
  • Data Integrity Gaps: Troubleshooting “how a value reached this state” becomes impossible if the audit trail only shows identity swaps.

Example or Code (if necessary and relevant)

// The problematic configuration
Javers javers = JaversBuilder.javers()
    .registerEntity(EntityDefinitionBuilder.entityDefinition(Entity.class)
        .withIdPropertyName("id")
        .build())
    .build();

// The resulting incorrect JSON change log
{
  "changeType": "PROPERTY_VALUE_CHANGED",
  "propertyName": "product",
  "left": { "typeName": "Product", "cdoId": 1 },
  "right": { "typeName": "Product", "cdoId": 3 }
}

How Senior Engineers Fix It

A senior engineer approaches this by decoupling Identity from Auditability. To fix this while still allowing Product to be audited independently, we use one of the following strategies:

  • Manual Shadowing/Value Object Mapping: If Product is frequently embedded, define a ProductValue version of the class (or a projection) registered as a Value Object for the purpose of the Foo audit.
  • Custom Javers Shadowing: Implement a JaversComparator or use @DiffIgnore on the ID field within specific contexts to force the engine to look deeper.
  • Explicit Audit Traversal: Instead of relying on the parent-child diff, ensure that every change to Product is committed via its own javers.commit(product) call. This ensures the Product timeline is populated regardless of whether it was updated via Foo or directly.
  • Refined Registration: Use registerValue for nested components that do not require identity-based tracking within the context of the parent, even if they are technically entities in the database.

Why Juniors Miss It

  • Focus on “What” vs “How”: Juniors focus on the fact that the code runs without errors and the ID change is visible, assuming the audit is “working.”
  • Identity Over-reliance: They tend to treat the Entity status as a “magic flag” that makes everything auditable, without realizing that Identity is the enemy of deep-diffing.
  • Lack of Graph Mental Model: They view objects as isolated units rather than nodes in a directed graph where the link type (Reference vs. Value) dictates the behavior of the auditing engine.

Leave a Comment