Spring BootAuditing
Spring Boot

Auditing

Spring Data JPA auditing automatically populates created and modified timestamps and user fields on entities. @EnableJpaAuditing activates the infrastructure; @CreatedDate, @LastModifiedDate, @CreatedBy, and @LastModifiedBy on entity fields are populated before every INSERT and UPDATE. This entry covers setup, a shared base entity, custom AuditorAware, event-based auditing with @EntityListeners, and full audit history with Hibernate Envers.

Setup and @EnableJpaAuditing

Add @EnableJpaAuditing to a @Configuration class or the main application class to activate Spring Data's auditing infrastructure. Once enabled, @EntityListeners(AuditingEntityListener.class) on an entity class — or on a @MappedSuperclass shared by all entities — instructs the listener to populate the annotated fields before every persist and merge.
Java
// ── Enable auditing ───────────────────────────────────────────────────
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaConfig {
    // auditorAwareRef wires the AuditorAware bean that supplies
    // the current user's identifier for @CreatedBy / @LastModifiedBy
}

// ── Alternatively on the main class ───────────────────────────────────
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

// ── AuditorAware — supplies the current principal ─────────────────────
@Component("auditorProvider")
public class SecurityAuditorAware implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.ofNullable(
                SecurityContextHolder.getContext().getAuthentication())
            .filter(Authentication::isAuthenticated)
            .filter(auth -> !"anonymousUser".equals(auth.getPrincipal()))
            .map(Authentication::getName);
    }
}

// ── For non-Spring-Security applications ──────────────────────────────
@Component("auditorProvider")
public class RequestContextAuditorAware implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        // Could read from a ThreadLocal, MDC, or request attribute
        return Optional.ofNullable(
            RequestContextHolder.getRequestAttributes())
            .map(attrs -> (ServletRequestAttributes) attrs)
            .map(ServletRequestAttributes::getRequest)
            .map(req -> (String) req.getAttribute("authenticatedUser"));
    }
}

Auditable Base Entity

Define a @MappedSuperclass that carries all four audit fields and annotate it with @EntityListeners(AuditingEntityListener.class). Every entity that extends it inherits auditing without any additional configuration. This is the most maintainable pattern for applications with many entities.
Java
// ── Shared auditable base ─────────────────────────────────────────────
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
@SuperBuilder
@NoArgsConstructor
public abstract class AuditableEntity {

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

    @LastModifiedDate
    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;

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

    @LastModifiedBy
    @Column(name = "updated_by", length = 100)
    private String updatedBy;
}

// ── Entity extending the base ─────────────────────────────────────────
@Entity
@Table(name = "products")
@Getter @Setter
@NoArgsConstructor
@SuperBuilder
public class Product extends AuditableEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private ProductStatus status;
}

// ── Result — what Hibernate sets automatically ─────────────────────────
// On first save():
//   created_at  = 2024-03-15T10:30:00
//   updated_at  = 2024-03-15T10:30:00
//   created_by  = "alice"
//   updated_by  = "alice"
//
// On subsequent save():
//   updated_at  = 2024-03-15T11:45:00   ← updated
//   updated_by  = "bob"                 ← updated
//   created_at  unchanged               ← updatable = false
//   created_by  unchanged               ← updatable = false

Auditing with UUID and Long Auditor Types

AuditorAware<T> is generic — T can be String (username), Long (user ID), or UUID (user UUID). Match T to the type of @CreatedBy and @LastModifiedBy fields on the entity. Using a Long or UUID user ID avoids storing mutable usernames and is better for foreign key references.
Java
// ── Long user ID auditing ─────────────────────────────────────────────
@Component("auditorProvider")
public class LongAuditorAware implements AuditorAware<Long> {

    @Override
    public Optional<Long> getCurrentAuditor() {
        return Optional.ofNullable(
                SecurityContextHolder.getContext().getAuthentication())
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getPrincipal)
            .filter(p -> p instanceof UserPrincipal)
            .map(p -> ((UserPrincipal) p).getId());
    }
}

// ── Base entity with Long auditor ─────────────────────────────────────
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class LongAuditableEntity {

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

    @LastModifiedDate
    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;

    @CreatedBy
    @Column(name = "created_by_id", updatable = false)
    private Long createdById;

    @LastModifiedBy
    @Column(name = "updated_by_id")
    private Long updatedById;
}

// ── Enable with explicit type ──────────────────────────────────────────
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaConfig {

    @Bean
    public AuditorAware<Long> auditorProvider() {
        return new LongAuditorAware();
    }
}

// ── UUID auditor ───────────────────────────────────────────────────────
@Component("auditorProvider")
public class UuidAuditorAware implements AuditorAware<UUID> {

    @Override
    public Optional<UUID> getCurrentAuditor() {
        return Optional.ofNullable(
                SecurityContextHolder.getContext().getAuthentication())
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getPrincipal)
            .filter(p -> p instanceof UserPrincipal)
            .map(p -> ((UserPrincipal) p).getUuid());
    }
}

Lifecycle Callbacks with @PrePersist and @PreUpdate

JPA lifecycle callbacks are an alternative to Spring Data's AuditingEntityListener when Spring context is not available — in tests, batch jobs, or non-Spring-managed environments. @PrePersist runs before INSERT; @PreUpdate runs before UPDATE. They live on the entity itself or on a separate EntityListener class.
Java
// ── Lifecycle callbacks directly on the entity ───────────────────────
@Entity
@Table(name = "orders")
@Getter @Setter @NoArgsConstructor
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal total;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
        this.createdAt = now;
        this.updatedAt = now;
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now(ZoneOffset.UTC);
    }
}

// ── External EntityListener class ─────────────────────────────────────
public class TimestampEntityListener {

    @PrePersist
    public void prePersist(Object entity) {
        if (entity instanceof Timestamped t) {
            LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
            t.setCreatedAt(now);
            t.setUpdatedAt(now);
        }
    }

    @PreUpdate
    public void preUpdate(Object entity) {
        if (entity instanceof Timestamped t) {
            t.setUpdatedAt(LocalDateTime.now(ZoneOffset.UTC));
        }
    }
}

// ── Interface for the listener to target ──────────────────────────────
public interface Timestamped {
    void setCreatedAt(LocalDateTime createdAt);
    void setUpdatedAt(LocalDateTime updatedAt);
}

// ── Register on a @MappedSuperclass ───────────────────────────────────
@MappedSuperclass
@EntityListeners(TimestampEntityListener.class)
public abstract class TimestampedEntity implements Timestamped {

    @Column(name = "created_at", updatable = false)
    @Setter private LocalDateTime createdAt;

    @Column(name = "updated_at")
    @Setter private LocalDateTime updatedAt;
}

Full Audit History with Hibernate Envers

Spring Data auditing records the current state only. Hibernate Envers records every historical revision — who changed what and when — in audit tables. Add the dependency, annotate entities with @Audited, and query history through AuditReader. Envers creates a shadow table for each audited entity with a _AUD suffix.
XML
<!-- pom.xml -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-envers</artifactId>
</dependency>

// ── Audited entity ────────────────────────────────────────────────────
@Entity
@Table(name = "products")
@Audited                             // Envers audits every change
@Getter @Setter @NoArgsConstructor
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private ProductStatus status;
}

// ── Envers creates these tables automatically ──────────────────────────
// REVINFO          — revision metadata (id, timestamp)
// PRODUCTS_AUD     — one row per revision per product
//   id, REV, REVTYPE (0=ADD, 1=MOD, 2=DEL), name, price, status

// ── Custom revision entity — add username to each revision ───────────
@Entity
@RevisionEntity(UserRevisionListener.class)
@Table(name = "revinfo")
@Getter @Setter
public class UserRevisionEntity extends DefaultRevisionEntity {

    @Column(name = "username", length = 100)
    private String username;
}

@Component
public class UserRevisionListener implements RevisionListener {

    @Override
    public void newRevision(Object revisionEntity) {
        UserRevisionEntity rev = (UserRevisionEntity) revisionEntity;
        Optional.ofNullable(
                SecurityContextHolder.getContext().getAuthentication())
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getName)
            .ifPresent(rev::setUsername);
    }
}

// ── Query audit history ────────────────────────────────────────────────
@Service
@RequiredArgsConstructor
public class ProductAuditService {

    @PersistenceContext
    private EntityManager entityManager;

    // All revisions of a product
    public List<ProductAuditEntry> findHistory(Long productId) {
        AuditReader reader = AuditReaderFactory.get(entityManager);
        return reader
            .createQuery()
            .forRevisionsOfEntity(Product.class, false, true)
            .add(AuditEntity.id().eq(productId))
            .addOrder(AuditEntity.revisionNumber().desc())
            .getResultList()
            .stream()
            .map(row -> {
                Object[]           tuple    = (Object[]) row;
                Product            product  = (Product) tuple[0];
                UserRevisionEntity revision = (UserRevisionEntity) tuple[1];
                RevisionType       type     = (RevisionType) tuple[2];
                return new ProductAuditEntry(
                    product, revision.getRevisionDate(),
                    revision.getUsername(), type.name());
            })
            .toList();
    }

    // State of a product at a specific revision
    public Product findAtRevision(Long productId, int revision) {
        AuditReader reader = AuditReaderFactory.get(entityManager);
        return reader.find(Product.class, productId, revision);
    }
}