Back to Entity Lifecycle & Auditing
Q1Question 1 of 2

What is JPA auditing?

Key Concepts
  • @CreatedDate and @LastModifiedDate stamp when the current row was created and last changed.
  • @CreatedBy and @LastModifiedBy stamp who changed it, requiring an AuditorAware bean for the current user.
  • @EnableJpaAuditing on a configuration class activates the Spring Data auditing mechanism.
  • Spring Data auditing tracks only the latest state on the current row: old values are overwritten.
  • Hibernate Envers @Audited maintains full revision history in separate audit tables (entity_AUD + REVINFO).
  • Envers allows querying any past state of an entity using AuditReader and revision numbers.
  • Use both together: Spring Data auditing for cheap current-row stamps, Envers for compliance history trails.

Definition

@CreatedDate and @LastModifiedDate stamp when the current row was last touched, while Hibernate Envers @Audited maintains full version history in separate audit tables for every change.
  • @CreatedDate -- automatically sets the creation timestamp when an entity is first persisted.
  • @LastModifiedDate -- automatically updates the modification timestamp on every save.
  • @CreatedBy / @LastModifiedBy -- automatically sets who created or last modified the entity, pulling the username from an AuditorAware bean.
  • @EnableJpaAuditing -- Spring Boot annotation that activates the auditing mechanism.
  • @EntityListeners(AuditingEntityListener.class) -- registers the listener that fills the auditing fields.
  • Hibernate Envers @Audited -- maintains full revision history by writing every version of an entity to separate audit tables.
  • AuditReader -- Envers API for querying historical revisions of an entity.

At National Bank of India, Spring Data auditing is like writing "last updated by Priya on 23-Oct, created by Rajesh on 1-Jan" in the margin of the current passbook page. You see who last touched it and when, but not the history of changes. Hibernate Envers is like keeping a photocopy of the passbook every single time anything changes, filed in a separate archive room. With Envers you can pull up exactly what Rajesh's balance was on any date in the past and who changed it.

Understanding the Concepts

What is Spring Data JPA auditing?

Spring Data JPA auditing automatically fills timestamp and user fields on entities when they are created or modified. It requires three pieces of setup: the @EnableJpaAuditing annotation on a configuration class, the @EntityListeners annotation on the entity, and the auditing field annotations on the entity fields.

  • @CreatedDate: set once when the entity is first persisted.
  • @LastModifiedDate: updated on every save.
  • @CreatedBy: set once with the creating user.
  • @LastModifiedBy: updated on every save with the modifying user.
JAVA
package com.bank.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Optional;

@Configuration
@EnableJpaAuditing
public class AuditConfig {

    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> Optional.ofNullable(
            SecurityContextHolder.getContext()
                .getAuthentication()
                .getName()
        );
    }
}
JAVA
package com.bank.model;

import jakarta.persistence.*;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.math.BigDecimal;
import java.time.Instant;

@Entity
@Table(name = "accounts")
@EntityListeners(AuditingEntityListener.class)
public class Account {

    @Id
    @Column(name = "account_id")
    private String accountId;

    @Column(name = "holder_name")
    private String holderName;

    @Column(precision = 15, scale = 2)
    private BigDecimal balance;

    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private Instant createdAt;

    @LastModifiedDate
    @Column(name = "updated_at")
    private Instant updatedAt;

    @CreatedBy
    @Column(name = "created_by", updatable = false)
    private String createdBy;

    @LastModifiedBy
    @Column(name = "updated_by")
    private String updatedBy;
}
TEXT
First save (Rajesh creates the account):
  created_at  = 2024-01-15 10:30:00
  updated_at  = 2024-01-15 10:30:00
  created_by  = "rajesh"
  updated_by  = "rajesh"

Second save (Priya updates the address):
  created_at  = 2024-01-15 10:30:00  (unchanged)
  updated_at  = 2024-03-20 14:15:00  (updated)
  created_by  = "rajesh"             (unchanged)
  updated_by  = "priya"              (updated)

Old values are overwritten. You cannot see what the balance was before.

What is Hibernate Envers?

Hibernate Envers maintains full revision history by creating audit tables that store every version of an entity. When an entity annotated with @Audited is inserted, updated, or deleted, Envers writes a revision row capturing the entire state at that point. You can later query any past revision to see exactly what the entity looked like.

  • Add the hibernate-envers dependency.
  • Annotate the entity with @Audited.
  • Hibernate creates entity_AUD tables and a shared REVINFO table.
  • Every INSERT, UPDATE, and DELETE writes a revision row.
JAVA
package com.bank.model;

import jakarta.persistence.*;
import org.hibernate.envers.Audited;
import java.math.BigDecimal;

@Entity
@Table(name = "accounts")
@Audited
public class Account {

    @Id
    @Column(name = "account_id")
    private String accountId;

    @Column(name = "holder_name")
    private String holderName;

    @Column(precision = 15, scale = 2)
    private BigDecimal balance;

    @Column(length = 20)
    private String status;
}
DIAGRAM
Tables created by Envers:

accounts_AUD
├── account_id     VARCHAR(20)
├── REV            INTEGER  (FK -> REVINFO)
├── REVTYPE        TINYINT  (0=INSERT, 1=UPDATE, 2=DELETE)
├── holder_name    VARCHAR
├── balance        DECIMAL(15,2)
└── status         VARCHAR(20)

REVINFO
├── REV            INTEGER  (PK, auto-increment)
└── REVTSTMP       BIGINT   (epoch millis)

How do you query history with Envers AuditReader?

The AuditReader API lets you query any past revision of an entity, list all revisions for an entity, or find the entity state at a specific point in time.

JAVA
package com.bank.service;

import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.envers.query.AuditEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class AuditHistoryService {

    @PersistenceContext
    private EntityManager em;

    @Transactional(readOnly = true)
    public Account getAccountAtRevision(String accountId, int revision) {
        AuditReader reader = AuditReaderFactory.get(em);
        return reader.find(Account.class, accountId, revision);
        // Returns the account as it looked at that revision
    }

    @Transactional(readOnly = true)
    @SuppressWarnings("unchecked")
    public List<Number> getAllRevisions(String accountId) {
        AuditReader reader = AuditReaderFactory.get(em);
        return reader.getRevisions(Account.class, accountId);
        // Returns [1, 5, 12, 23] - all revision numbers
    }

    @Transactional(readOnly = true)
    @SuppressWarnings("unchecked")
    public List<Account> findAccountsModifiedByUser(String username) {
        AuditReader reader = AuditReaderFactory.get(em);
        return reader.createQuery()
            .forRevisionsOfEntity(Account.class, true, true)
            .add(AuditEntity.revisionProperty("username").eq(username))
            .getResultList();
    }
}
TEXT
Query: getAccountAtRevision("SAV2001", 5)
SQL: SELECT * FROM accounts_AUD
     WHERE account_id = 'SAV2001' AND REV = 5

Returns the Account object with the exact field values at revision 5.

How is Spring Data auditing different from Envers?

| Aspect | Spring Data Auditing | Hibernate Envers |
|---|---|---|
| What it tracks | When and who (4 columns) | Full entity state (every field) |
| Storage | Same table (current row) | Separate _AUD tables |
| History | Latest change only | Every version preserved |
| Old values | Overwritten | Stored in revision rows |
| Performance cost | Minimal (4 column updates) | Higher (extra INSERT per change) |
| Setup complexity | Simple (annotations + config) | Moderate (dependency + _AUD tables) |
| Query past state | Not possible | Yes, via AuditReader |
| Compliance / audit trail | Insufficient for regulators | Meets regulatory requirements |
| Dependency | Spring Data JPA (included) | hibernate-envers (extra) |

JAVA
package com.bank.model;

import jakarta.persistence.*;
import org.hibernate.envers.Audited;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.math.BigDecimal;
import java.time.Instant;

// BOTH can coexist on the same entity
@Entity
@Table(name = "accounts")
@Audited
@EntityListeners(AuditingEntityListener.class)
public class Account {

    @Id
    @Column(name = "account_id")
    private String accountId;

    @Column(precision = 15, scale = 2)
    private BigDecimal balance;

    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private Instant createdAt;  // Spring Data auditing

    @LastModifiedDate
    @Column(name = "updated_at")
    private Instant updatedAt;  // Spring Data auditing

    // Envers tracks the full state in accounts_AUD
    // Spring Data stamps track when/who on the current row
}

What are common mistakes with auditing?

The most common mistakes involve missing configuration and incorrect field annotations.

JAVA
// WRONG: @CreatedDate without @EnableJpaAuditing
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Account {
    @CreatedDate
    private Instant createdAt;
    // createdAt is NEVER set because auditing is not enabled!
}

// CORRECT: add @EnableJpaAuditing to a config class
@Configuration
@EnableJpaAuditing
public class AuditConfig {}
JAVA
// WRONG: @CreatedDate without @EntityListeners
@Entity
public class Account {
    @CreatedDate
    private Instant createdAt;
    // No listener registered, createdAt stays null
}

// CORRECT: add @EntityListeners
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Account {
    @CreatedDate
    private Instant createdAt;
}
JAVA
// WRONG: @CreatedDate without updatable=false
@CreatedDate
@Column(name = "created_at")
private Instant createdAt;
// On every update, created_at gets overwritten!

// CORRECT: mark as updatable=false
@CreatedDate
@Column(name = "created_at", updatable = false)
private Instant createdAt;
// Set once on INSERT, never changed on UPDATE

When should you use each approach?

Use Spring Data auditing when you need cheap, lightweight current-row timestamps and user stamps. Use Envers when regulators require a full audit trail showing every historical state. In banking, both are typically used together.

JAVA
// Spring Data auditing alone:
// - Sufficient for "when was this last changed?"
// - Cheap: 4 extra columns, no extra tables
// - NOT sufficient for regulatory compliance

// Envers alone:
// - Full history, every version preserved
// - Heavier: extra tables, extra INSERTs
// - Answers "what was the balance on March 15?"

// Both together (recommended for banking):
// - Current-row stamps for quick support queries
// - Full Envers history for compliance and forensics
// - Envers audit tables grow over time: plan retention

What about custom revision entities in Envers?

Envers allows you to extend the REVINFO table with custom fields like IP address, request ID, or channel name via a custom revision entity and a RevisionListener. This is invaluable for forensic investigations in banking.

JAVA
package com.bank.audit;

import jakarta.persistence.*;
import org.hibernate.envers.RevisionEntity;
import org.hibernate.envers.DefaultRevisionEntity;

@Entity
@Table(name = "revision_info")
@RevisionEntity(BankRevisionListener.class)
public class BankRevision extends DefaultRevisionEntity {

    @Column(name = "username")
    private String username;

    @Column(name = "ip_address")
    private String ipAddress;

    @Column(name = "request_id")
    private String requestId;

    @Column(name = "channel")
    private String channel;  // "mobile", "web", "branch"

    // getters and setters
}
JAVA
package com.bank.audit;

import org.hibernate.envers.RevisionListener;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class BankRevisionListener implements RevisionListener {

    @Override
    public void newRevision(Object revisionEntity) {
        BankRevision rev = (BankRevision) revisionEntity;
        rev.setUsername(SecurityContextHolder.getContext()
            .getAuthentication().getName());

        ServletRequestAttributes attrs = (ServletRequestAttributes)
            RequestContextHolder.getRequestAttributes();
        if (attrs != null) {
            rev.setIpAddress(attrs.getRequest().getRemoteAddr());
            rev.setRequestId(attrs.getRequest().getHeader("X-Request-Id"));
            rev.setChannel(attrs.getRequest().getHeader("X-Channel"));
        }
    }
}
DIAGRAM
Custom revision_info table:
├── REV         INTEGER  (PK)
├── REVTSTMP    BIGINT
├── username    VARCHAR
├── ip_address  VARCHAR
├── request_id  VARCHAR
└── channel     VARCHAR

Now every revision records WHO changed it, from WHERE,
via which CHANNEL, and with which REQUEST.

What about retention and performance?

Envers audit tables grow forever because they store every version of every audited entity. In banking, regulators often require 7-year retention, but keeping all that data in the operational database degrades performance. Plan a retention and archival strategy.

JAVA
// Retention strategy for banking:
//
// 1. Keep recent revisions (< 1 year) in the operational DB
//    for fast queries and support investigations.
//
// 2. Archive older revisions to cold storage (S3, Glacier)
//    on a scheduled basis.
//
// 3. Index the audit tables on REV and entity ID for
//    efficient AuditReader queries.
//
// 4. Monitor audit table sizes and query times.
//    accounts_AUD with 10M revisions can slow down if not indexed.
//
// 5. Consider partitioning audit tables by year
//    for easier archival and faster queries.

Visual Overview

DIAGRAM
  Spring Data Auditing:
  ┌─────────────────────────────────────────────┐
  │  accounts (same table)                       │
  │  ├── account_id: SAV2001                     │
  │  ├── balance: 150000.00                      │
  │  ├── created_at: 2024-01-15  (set once)      │
  │  ├── updated_at: 2024-03-20  (overwritten)   │
  │  ├── created_by: rajesh      (set once)      │
  │  └── updated_by: priya       (overwritten)   │
  └─────────────────────────────────────────────┘
  Old values are GONE. Cannot see previous balance.

  Hibernate Envers:
  ┌─────────────────────┐     ┌──────────────────────────────┐
  │  accounts            │     │  accounts_AUD                 │
  │  (current state)     │     │  (full history)               │
  │  balance: 150000     │     │  REV=1: balance=50000  (Jan)  │
  └─────────────────────┘     │  REV=2: balance=100000 (Feb)  │
                              │  REV=3: balance=150000 (Mar)  │
                              └──────────────────────────────┘
                              ┌──────────────────────────────┐
                              │  REVINFO                      │
                              │  REV=1: 2024-01-15, rajesh    │
                              │  REV=2: 2024-02-10, priya     │
                              │  REV=3: 2024-03-20, priya     │
                              └──────────────────────────────┘

Key Takeaway

There are two layers of auditing in JPA and they answer different questions. Spring Data auditing uses @CreatedDate, @LastModifiedDate, @CreatedBy, and @LastModifiedBy with an AuditingEntityListener to stamp when and who on the current row. It is cheap, adds only four columns, and answers "when was this last touched and by whom." But it overwrites old values, so you cannot see what the balance was before the latest change.

Hibernate Envers is a different tool for a different need. By adding the @Audited annotation and the hibernate-envers dependency, Hibernate maintains full revision history in separate audit tables. Every insert, update, and delete writes a revision row, so you can use AuditReader to reconstruct exactly what an entity looked like at any past revision. The cost is higher because every change writes an extra row, and audit tables grow forever.

In our National Bank of India system, both are used together. The current account row carries created and updated stamps so support staff can see at a glance when it last changed. Envers keeps the compliance history so that when a regulator asks what Rajesh's balance was on March 15 and who changed it, we can answer precisely. Banking regulations frequently mandate seven-year audit trails, and current-row stamps cannot satisfy that because old values are overwritten.

Remember one sentence: Spring Data auditing stamps when and who on the current row for convenience, while Envers @Audited preserves every historical version in separate audit tables for compliance and forensic queries.

---

Share
Use ← → arrow keys to navigate