Definition
- @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
AuditorAwarebean. - @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.
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()
);
}
}
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;
}
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-enversdependency. - Annotate the entity with
@Audited. - Hibernate creates
entity_AUDtables and a sharedREVINFOtable. - Every INSERT, UPDATE, and DELETE writes a revision row.
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;
}
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.
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();
}
}
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) |
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.
// 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 {}
// 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;
}
// 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.
// 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.
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
}
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"));
}
}
}
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.
// 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
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.
---