Auditing, Soft Deletes and Versioning in JPA

Site Admin · 11 Sep 2026 · 8 views

Auditing, Soft Deletes and Versioning in JPA

Knowing who changed a record and when is a common requirement in regulated industries. JPA provides built-in auditing with @CreatedDate, @LastModifiedDate, and @Version. Combined with soft deletes, you get a complete history of your data without ever losing a row.

Enabling JPA Auditing

Spring Data JPA makes auditing straightforward. Annotate a base class with audit fields, enable auditing in configuration, and Spring handles the rest:

import jakarta.persistence.*;
import org.springframework.data.annotation.*;
import java.time.LocalDateTime;

@MappedSuperclass
public abstract class Auditable {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedAt;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String modifiedBy;

    // getters and setters
}

@Entity
@Table(name = "products")
public class Product extends Auditable {

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

    private String name;
    private double price;

    // getters and setters
}

You also need an auditor provider. The simplest approach is to implement AuditorAware<String> that returns the current user from the security context. Spring will inject the correct values automatically on persist and update.

Soft Deletes with @SQLDelete

Soft deletes mark a row as deleted without removing it. Hibernate does not support this natively, but the @SQLDelete and @Where annotations from Hibernate Extras let you override the DELETE statement and filter out soft-deleted rows from queries:

import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

@Entity
@Table(name = "orders")
@SQLDelete(sql = "UPDATE orders SET deleted = true WHERE id = ?")
@Where(clause = "deleted = false")
public class Order {

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

    private String product;
    private double total;
    private boolean deleted;

    // getters and setters
}

Now when you call entityManager.remove(order), Hibernate generates UPDATE orders SET deleted = true WHERE id = ? instead of DELETE. All normal queries automatically exclude deleted rows because of the @Where clause. You can still query deleted rows by using a native SQL query if you need an admin audit view.

Version-Based Conflict Detection

The @Version annotation adds a version column that Hibernate increments on every UPDATE. This serves double duty: it detects optimistic locking conflicts and also acts as an audit trail of how many times a row has changed:

@Entity
@Table(name = "products")
public class Product {

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

    private String name;

    @Version
    private long version;

    // getters and setters
}

Every UPDATE sets version = version + 1. If two transactions try to update the same row, the second one fails with OptimisticLockException because the version no longer matches. This prevents lost updates without holding any database locks.

Audit Table Pattern

For full change history, create a separate audit table that stores every version of a row. Use Hibernate event listeners or a library like Envers to capture INSERT, UPDATE, and DELETE events and write them to the audit table with timestamps and user info.

Key Points

  • Extend Auditable with @CreatedDate and @LastModifiedDate for automatic timestamps.
  • Implement AuditorAware to track which user performed each change.
  • Soft deletes use @SQLDelete to turn DELETE into UPDATE and @Where to filter results.
  • @Version provides optimistic locking and tracks the number of changes per row.
  • Audit tables or Envers can store full change history for regulatory compliance.
Share this post:

Comments (0)

Please login or register to comment.